#!/usr/bin/perl -w
### TODO NEXT: Archive::Zip / Algorithm::Diff

=head1 NAME

addonpkg - Fetch, diff and install addons from svn servers.

B<addonpkg> downloads addons to a stage area, allows you to see changes and svn logs
before installing updated addons. Supports archiving groups of addons. Can automatically
split modules out of many addons.

=head1 SYNOPSIS

addonpkg [actions] [options] [addons ...]

  Actions:
    -archive <f>   Archive the addons to file <f>.
    -defembed      Reset addons to default embedded state.
    -defsplit      Reset addons to default split state.
    -diff          Compare addons in staging area with installed addons.
    -disable       Disable the addon.
    -disembed      Do not embed externals inside the addon.
    -dump          Debugging option used for this script.
    -embed         Embed externals inside the addon.
    -enable        Enable the addon.
    -fetch         Retrieve addons from svn server.
    -help          Displays brief usage of addonpkg.
    -install       Install addons from staging area.
    -list          List addons.
    -log           Display log of changes between staging area and installed.
    -man           Displays the full man page for addonpkg.
    -nosplit       Makes the addons listed no longer split when installed.
    -setalias <a>  Makes <a> an alias to the listed addons.
    -setgroup <g>  Adds the addons to group <g>.
    -split         Makes the addons listed split when installed.
    -uninstall     Uninstalls addons from the destination directory.
    -update        Install addons only if the have been previously installed.
    -users         List which installed addons use the named addons.

  Options:
   Filters:
    -author <a>    Only apply to addons by author
    -category <c>  Only apply to addons in category.
    -installed     Only apply to addons that are already installed.
    -interface <#> Only apply to addons at interface # level.
    -svn <url>     Only apply to addons from SVN url.
    -r <rev>       Revision to fetch.
    -svnonly       Only consider addons fetched with SVN.
    -uninstalled   Only apply to addons that are not installed.
   Paths:
    -d <dir>       Destination directory
    -s <dir>       Stage directory.
   Source:
    -git <url>     GIT url.
    -hg <url>      Mercurial url.
    -r <rev>       Revision to fetch.
    -svn <url>     SVN url.
   Output:
    -columns <#>   The width of the terminal.
    -details       Prints addition details, implies -list.
    -f <font>      Figlet font used by the -diff function.
    -other         Installed addons not in stage area, implies -list.
    -outdated      Displays list of outdated addons, implies -list.
    -rows <#>      The height of the terimnal.
    -w <#>         The width of the terminal.
    -v             Print commands as they are run.
   Other:
    -force         Force overwriting when fetching.
    -n             No execute, print commands to be run only.

  AddOns:
    -all           Same as specifying everything in the staging dir.
    -downloads     Same as specifying manually downloads addons.
    -libs          Same as specifying all libraries used by the named addons.
    name           Operates on the addon name from the staging dir.
    path           Operates on the addon using the path name.

=cut

# Modules
use strict;
use FindBin;
use lib "$FindBin::Bin/lib", "$FindBin::RealBin/lib";

use File::Spec;
use File::Path;
use File::Copy;
use File::Temp;
use File::Slurp;
use Text::ParseWords;
use Getopt::Long;
use IO::File;
use IO::Dir;
use IO::Pipe;
use Date::Format;
use URI;
use Cwd qw{cwd abs_path};
use LWP;
use Pod::Usage;
use List::MoreUtils qw(none);
use Errno;
use Fcntl ':mode';

use Constants qw( :dir :suffix :toc :vinfo :state :source );
use Options;
use FormatString;
use Database;
use AddonSVN;
use AddonHG;
use AddonGIT;
use AddonInfo;
use TOCInfo;
use AddonPath;
use AddonTemp;
use AddonDiff;
use AddonKey;
use NodePath;
use AddonSubdirectory;

use Carp;

#$SIG{"__WARN__"} = sub { Carp::cluck(@_); };
#$SIG{"__DIE__"} = sub { Carp::confess(@_); };

BEGIN {
    eval { require XML::Parser::Expat; } and eval { import XML::Parser::Expat; };
}
my $has_xml_expat = ( defined &XML::Parser::Expat::new );

BEGIN {
    eval { require Data::Dumper; } and eval { import Data::Dumper; $Data::Dumper::Sortkeys = 1; $Data::Dumper::Indent = 1; };
}
my $has_dumper = ( defined &Data::Dumper::new );

my $ua = LWP::UserAgent->new( agent => "addonpkg/1.0 " );

# Constants

# Options

# Prototypes

sub cmp_caseless { uc( $a ) cmp uc( $b ) }
sub get_paths_for_all_externals( );
sub mark_external_path_used($ );
sub get_path_for_external($;$ );

sub expand_addon_names(@ );

sub do_op_compare_number($$$ );
sub do_op_compare_uri($$$ );

sub check_toc_filter($ );

sub get_subdirs_in_dir($;$ );
sub get_addon_externals($$ );

sub fetch_uri_raw($$$;$$$ );
sub fetch_addon($ );

sub find_externals_for_addon($ );

sub lua_unescape($ );
sub lua_escape($;$$ );
sub parse_lua_string($ );
sub parse_lua_key($ );
sub parse_lua_locale_assignment($ );
sub parse_lua_locale_table($$ );
sub parse_lua_locale_additions($$ );
sub parse_lua_locale($$ );
sub dump_lua_table($$% );

sub read_locale($;$$ );
sub replace_locale($$ );
sub replace_node($;$ );

sub read_addon($$$ );
sub read_addon_node($$$$;$ );

sub read_staged_addon($ );
sub read_staged_addon_path($;$ );
sub read_installed_addon($ );

sub dump_addon($$ );
sub dump_addon_node($$ );

sub install_addon($$$;$$ );
sub install_addon_node($$ );

sub reinstall_addon($$$;$ );

sub uninstall_addon($$$;$ );

sub compare_addon($$$$;$ );
sub compare_addon_nodes($$$;$ );

sub log_addon($$$ );

sub figlet(@ );
sub next_output_header(@ );
sub output(@ );
sub finish_output( );

sub get_character_lists( );
sub get_enabled_lists( );
sub get_addon_enabled_disabled_default_count($ );
sub set_addon_state($$ );
sub disable_addon($ ) { set_addon_state( shift, &STATE_DISABLED ); }
sub enable_addon($ )  { set_addon_state( shift, &STATE_ENABLED ); }

# Argument parsing
sub map_argument($$$ );
sub map_arguments($$@ );
sub name_of_arguments(@ );
sub process_action_flag($$ );
sub process_filter_option($$ );
sub process_path_option($$ );
sub process_source_option($$ );
sub process_output_option($$ );
sub process_flag_option($$ );
sub process_string_option($$ );

# Global variables
my $exit_res              = 0;
my $given_addon_table_all = {};
my $given_addon_table     = {};
my $src_addon_table_all   = {};
my $src_addon_table       = {};
my $dest_addon_table      = {};
my $externals_table       = {};
my @to_comment            = ();
my $need_install_dir;
my $need_read_staged_addon;
my $need_read_installed_addon;
my $need_read_all_installed;
my $need_process_addon;
my $need_compare_addon;
my %force_embed_url   = ();
my %unforce_embed_url = ();

my %lua_escape = (
    "a" => "\007",
    "b" => "\010",
    "f" => "\014",
    "n" => "\012",
    "r" => "\015",
    "t" => "\011",
    "v" => "\013",
);

my %finder_special_names = (
    uc &DIR_OSX_ICON => 1,
    uc ".DS_Store"   => 1,
);

my %external_special_names = (
    &DIR_UC_SVN => 1,
    &DIR_UC_GIT => 1,
    &DIR_UC_HG  => 1,
);

sub get_paths_for_all_externals( ) {
    my $root = File::Spec->catdir( stage_dir, &DIR_EXTERNALS );

    my @to_check = ( $root, undef );

    foreach my $tree ( values %$externals_table ) {
        $tree->{ mark_used } = 0;
    }

    while ( @to_check ) {
        my $check_path = shift @to_check;
        my $check_tree = shift @to_check;

        my $dirh = IO::Dir->new( $check_path );
        next unless $dirh;

        foreach my $name ( File::Spec->no_upwards( $dirh->read ) ) {
            next if $finder_special_names{ uc $name };

            if ( $external_special_names{ uc $name } ) {
                if ( defined $check_tree ) {
                    $check_tree->{ controlled } = 1;
                }
                next;
            } ## end if ( $external_special_names...)

            my $sub_name = File::Spec->catdir( $check_path, $name );
            next unless -d $sub_name;

            my $new_tree = $externals_table->{ $sub_name } //= {};
            if ( defined $check_tree ) {
                $check_tree->{ children }->{ $name } //= $new_tree;
                if ( $check_tree->{ used } ) {
                    $new_tree->{ used } = 1;
                }
                else {
                    if ( $new_tree->{ used } ) {
                        mark_external_path_used( $sub_name );
                    }
                }
            } ## end if ( defined $check_tree)

            $new_tree->{ path } = $sub_name;
            $new_tree->{ children } //= {};
            $new_tree->{ used } //= 0;
            $new_tree->{ parent } = $check_tree;

            $externals_table->{ $sub_name } = $new_tree;

            push @to_check, $sub_name, $new_tree;
        } ## end foreach my $name ( File::Spec...)
    } ## end while ( @to_check )
} ## end sub get_paths_for_all_externals( )

sub mark_external_path_used($ ) {
    my $path = shift;

    my $external_tree = $externals_table->{ $path };
    return unless $external_tree;

    # Search for the highest controlled directory if there is one
    my $root_tree = $external_tree;
    my $try_root  = $external_tree;
    while ( $try_root ) {
        if ( $try_root->{ controlled } ) {
            $root_tree = $try_root;
        }
        $try_root = $try_root->{ parent };
    } ## end while ( $try_root )

    # Handle children
    my @to_check = ( $root_tree );
    while ( @to_check ) {
        my $check_tree = shift @to_check;
        my $sub_name   = $check_tree->{ path };
        my $children   = $check_tree->{ children };
        unless ( $check_tree->{ used } ) {

            #print STDERR "Using(c): ", $sub_name, "\n";
        }
        $check_tree->{ used } = 1;
        push @to_check, values %$children;
    } ## end while ( @to_check )

    # Handle parents
    while ( $root_tree ) {
        my $sub_name = $root_tree->{ path };
        unless ( $root_tree->{ used } ) {

            #print STDERR "Using(p): ", $sub_name, "\n";
        }
        $root_tree->{ used } = 1;
        $root_tree = $root_tree->{ parent };
    } ## end while ( $root_tree )
} ## end sub mark_external_path_used($ )

sub get_path_for_external($;$ ) {
    my $url      = shift;
    my $no_mkdir = shift;
    $url =~ s{/*$}{};
    my $uri  = URI->new( $url )->canonical;
    my $auth = $uri->authority;
    $auth =~ s{.*@}{};
    $auth =~ s{:.*}{};

    my $auth_piece = $auth;
    my $up_depth   = 0;
    my @pieces     = ();
    if ( $auth eq q{} || !File::Spec->no_upwards( $auth ) ) {
        $auth_piece = "unknown";
    }

    foreach my $piece ( $uri->path_segments ) {
        if ( $piece eq q{} || $piece eq q{.} ) {
            next;
        }
        elsif ( $piece eq q{..} ) {
            if ( @pieces ) {
                pop @pieces;
            }
            else {
                ++$up_depth;
            }
        } ## end elsif ( $piece eq q{..} )
        elsif ( !File::Spec->no_upwards( $piece ) ) {
            push @pieces, "unknown";
        }
        else {
            push @pieces, $piece;
        }
    } ## end foreach my $piece ( $uri->path_segments)
    if ( $up_depth ) {
        unshift @pieces, "up" . $up_depth;
    }
    unshift @pieces, $auth_piece;

    my $path = File::Spec->catdir( stage_dir, &DIR_EXTERNALS, @pieces );
    pop @pieces;
    my $parent = File::Spec->catdir( stage_dir, &DIR_EXTERNALS, @pieces );
    unless ( $no_mkdir ) {
        mkpath( $parent, $opt_output{ "verbose" }, 0777 ) unless -d $parent;
    }
    return $path;
} ## end sub get_path_for_external($;$ )

# Convert between addon names and path
sub expand_addon_names(@ ) {
    my @res       = ();
    my %NAME_SEEN = ();
    while ( @_ ) {
        my $name   = shift @_;
        my @pieces = File::Spec->splitdir( $name );
        if ( @pieces == 1 ) {
            next if $NAME_SEEN{ $name };
            $NAME_SEEN{ $name } = 1;

            my $info = AddonInfo->lookup( $name );
            if ( $info ) {
                if ( $info->is_libname ) {
                    push @_, $info->get_base_addon;
                }
                elsif ( $info->is_alias ) {
                    push @_, $info->get_alias;
                }
                elsif ( $info->is_split ) {
                    my $base = $info->get_base_addoninfo;
                    if ( $base && !$base->is_split_allowed ) {
                        push @res, $base->get_name;
                    }
                    else {
                        push @res, $name;
                    }
                } ## end elsif ( $info->is_split )
                else {
                    push @res, $name;
                }
            } ## end if ( $info )
            else {
                if ( AddonInfo->is_group( $name ) ) {
                    my $path = File::Spec->catdir( stage_dir, $name );
                    push @_, get_subdirs_in_dir( $path, 1 );
                }
                elsif ( uc $name eq uc &DIR_DOWNLOADS ) {
                    push @_, get_downloads( stage_dir );
                }
                else {
                    push @res, $name;
                }
            } ## end else [ if ( $info ) ]
        } ## end if ( @pieces == 1 )
        else {
            push @res, $name;
        }
    } ## end while ( @_ )
    return @res;
} ## end sub expand_addon_names(@ )

# Filter compare operation
my %ncomps = (
    ""   => sub { $a == $b },
    "==" => sub { $a == $b },
    "="  => sub { $a == $b },
    "<"  => sub { $a < $b },
    "<=" => sub { $a <= $b },
    ">"  => sub { $a > $b },
    ">=" => sub { $a >= $b },
    "!=" => sub { $a != $b },
    "!"  => sub { $a != $b },
    "<>" => sub { $a != $b },
    "><" => sub { $a != $b }
);
my %scomps = (
    ""   => sub { $a eq $b },
    "==" => sub { $a eq $b },
    "="  => sub { $a eq $b },
    "<"  => sub { $a lt $b },
    "<=" => sub { $a le $b },
    ">"  => sub { $a gt $b },
    ">=" => sub { $a ge $b },
    "!=" => sub { $a ne $b },
    "!"  => sub { $a ne $b },
    "<>" => sub { $a ne $b },
    "><" => sub { $a ne $b }
);

sub do_op_compare_number($$$ ) {
    my $comp = shift;
    local ( $a, $b ) = @_;
    my $func = ( $a =~ m{^\d+$} && $b =~ m{^\d+$} ) ? $ncomps{ $comp } : $scomps{ $comp };
    return $func && &$func;
} ## end sub do_op_compare_number($$$ )

sub do_op_compare_uri($$$ ) {
    my $comp = shift;
    local ( $a, $b ) = @_;
    my $au = URI->new( $a );
    $a = $au->canonical->as_string if $au;
    my $bu = URI->new( $b );
    $b = $bu->canonical->as_string if $bu;
    my $func = $scomps{ $comp };
    return $func && &$func;
} ## end sub do_op_compare_uri($$$ )

sub check_toc_filter($ ) {
    my $toc = shift;
    if ( exists $opt_filters{ "author" } ) {
        $toc = TOCInfo->load( $toc )
            or return undef;
        my $toc_author = $toc->get_tag( &TOC_AUTHOR )
            or return undef;
        my $comp = $opt_filters{ "author-op" } || "";
        my $val = $opt_filters{ "author" };
        if ( $comp eq "!" || $comp eq "!=" || $comp eq "<>" || $comp eq "><" ) {
            return undef unless ( $toc_author !~ m{\b$opt_filters{"author"}\b}i );
        }
        else {
            return undef unless ( $toc_author =~ m{\b$opt_filters{"author"}\b}i );
        }
    } ## end if ( exists $opt_filters...)
    if ( exists $opt_filters{ "category" } ) {
        $toc = TOCInfo->load( $toc )
            or return undef;
        my $toc_category = $toc->get_tag( &TOC_CATEGORY )
            or return undef;
        my $comp = $opt_filters{ "category-op" } || "";
        my $val = $opt_filters{ "category" };
        if ( $comp eq "!" || $comp eq "!=" || $comp eq "<>" || $comp eq "><" ) {
            return undef unless ( $toc_category !~ m{\b$opt_filters{"category"}\b}i );
        }
        else {
            return undef unless ( $toc_category =~ m{\b$opt_filters{"category"}\b}i );
        }
    } ## end if ( exists $opt_filters...)
    if ( $opt_filters{ "interface" } ) {
        $toc = TOCInfo->load( $toc )
            or return undef;
        my $toc_interface = $toc->get_tag( &TOC_INTERFACE )
            or return undef;
        return undef unless $toc_interface =~ m{^(\d+)$};
        $toc_interface = $1;
        my $comp = $opt_filters{ "interface-op" } || "";
        my $val = $opt_filters{ "interface" };
        $toc_interface += 9000 if ( $toc_interface < 1900 && $toc_interface >= 1000 );
        $val           += 9000 if ( $val < 1900           && $val >= 1000 );
        return undef unless do_op_compare_number( $comp, $toc_interface, $val );
    } ## end if ( $opt_filters{ "interface"...})
    return 1;
} ## end sub check_toc_filter($ )

# Get RSS feed
my $active_rss_feed_hash  = undef;
my $active_rss_feed_item  = undef;
my $active_rss_feed_field = undef;
my $active_rss_feed_value = undef;

sub read_rss_feed_xml_char($$ ) {
    my $parser = shift;
    my $text   = shift;

    return unless ( $active_rss_feed_field );
    $active_rss_feed_value .= $text;
} ## end sub read_rss_feed_xml_char($$ )

sub read_rss_feed_xml_start($$@ ) {
    my $parser = shift;
    my $elem   = shift;

    if ( $elem eq "item" ) {
        $active_rss_feed_item = { @_ };
    }
    elsif ( $active_rss_feed_item ) {
        $active_rss_feed_field = $elem;
        if ( @_ ) {
            my $rec = {};
            while ( @_ ) {
                my $key   = shift;
                my $value = shift;
                $rec->{ $key } = $value;
            }
            $active_rss_feed_value = $rec;
        } ## end if ( @_ )
        else {
            $active_rss_feed_value = "";
            $parser->setHandlers( 'Char' => \&read_rss_feed_xml_char );
        }
    } ## end elsif ( $active_rss_feed_item)
} ## end sub read_rss_feed_xml_start($$@ )

sub read_rss_feed_xml_end($$@ ) {
    my $parser = shift;
    my $elem   = shift;

    if ( $elem eq "item" ) {
        return unless ( $active_rss_feed_item );
        my $name = $active_rss_feed_item->{ "title" };
        $active_rss_feed_hash->{ $name } = $active_rss_feed_item;
        undef $active_rss_feed_item;
    } ## end if ( $elem eq "item" )
    elsif ( $active_rss_feed_field ) {
        my $old = $active_rss_feed_item->{ $active_rss_feed_field };
        if ( $old ) {
            $old = [ $old ] unless ref $old eq "ARRAY";
            push @$old, $active_rss_feed_value;
            $active_rss_feed_value = $old;
        }
        elsif ( $active_rss_feed_field =~ m{^wowaddon:(?:optionaldeps|dependencies|provides)$} ) {
            $active_rss_feed_value = [ $active_rss_feed_value ];
        }
        $active_rss_feed_item->{ $active_rss_feed_field } = $active_rss_feed_value;
        undef $active_rss_feed_field;
        undef $active_rss_feed_value;
    } ## end elsif ( $active_rss_feed_field)
} ## end sub read_rss_feed_xml_end($$@ )

sub read_rss_feed_xml($;$ ) {
    my $contents = shift;
    my $cache    = shift;

    my $parser = new XML::Parser::Expat;
    $parser->setHandlers(
        'Start' => \&read_rss_feed_xml_start,
        'End'   => \&read_rss_feed_xml_end
    );

    $active_rss_feed_hash  = $cache || {};
    $active_rss_feed_item  = undef;
    $active_rss_feed_field = undef;

    eval { $parser->parsestring( $contents ); };
    if ( $@ ) {
        $active_rss_feed_hash->{ "!" } = 1;
        return $active_rss_feed_hash;
    }
    $parser->release;
    return $active_rss_feed_hash;
} ## end sub read_rss_feed_xml($;$ )

my %rss_feed_cache     = ();
my %rss_feed_old_cache = ();

sub get_rss_feed_cache($;$ ) {
    return undef unless $has_xml_expat;
    my $uri   = URI->new( shift );
    my $force = shift;
    $force = 1 unless $has_dumper;
    undef $force if $opt_flags{ "dryrun" };
    undef $force if $opt_flags{ "weak" };
    my $root_uri = $uri->canonical->as_string;
    return undef unless $root_uri;
    my $db_name = $uri->opaque;
    $db_name =~ s{[^a-zA-Z0-9]+}{_}g;
    $db_name =~ s{^_+}{};
    $db_name =~ s{_$}{};
    $db_name = "rss." . $db_name;
    my $cache_file = database_file( $db_name );
    my $cache_is_old;

    my $cache = $rss_feed_cache{ $root_uri };
    my $old_cache;
    unless ( $cache ) {
        $old_cache = $rss_feed_old_cache{ $root_uri };
    }
    unless ( $cache ) {
        if ( -f $cache_file ) {
            $old_cache = do $cache_file;
        }
    }
    $cache = $old_cache unless $force;
    unless ( $cache ) {
        my $mtime = ( stat( $cache_file ) )[ 9 ];
        my $req = HTTP::Request->new( GET => $root_uri );
        $req->if_modified_since( $mtime ) if ( ( defined $mtime ) && ( defined $old_cache ) );
        my $res = $ua->request( $req );
        if ( $res->code == 200 ) {
            $cache = read_rss_feed_xml( $res->content );
            if ( $has_dumper && !$cache->{ "!" } ) {

                # Dump the cache
                my $dumper = Data::Dumper->new( [ $cache ] );
                update_database_file( $db_name, $dumper->Terse( 1 )->Dump, $res->last_modified );
            } ## end if ( $has_dumper && !$cache...)
            $rss_feed_cache{ $root_uri } = $cache;
        } ## end if ( $res->code == 200)
        elsif ( $res->code == 304 ) {
            $cache = $old_cache;
            $rss_feed_cache{ $root_uri } = $old_cache;
        }
        else {
            warn( "Unable to get feed: " . $root_uri . ": " . $res->status_line );
        }
    } ## end unless ( $cache )
    return -1 if ( $cache->{ "!" } );
    return $cache;
} ## end sub get_rss_feed_cache($;$ )

# Get addons from directory
sub get_subdirs_in_dir($;$ ) {
    my $path             = shift;
    my $include_archives = shift;
    my %names            = ();
    return () unless -d $path;
    my $dirh = IO::Dir->new( $path );
    return () unless $dirh;

    foreach my $name ( File::Spec->no_upwards( $dirh->read ) ) {
        next if $name =~ m{\.force$};
        next if $name =~ m{\.bak$};
        next if $name =~ m{^\.addonpkg}i;
        next if $name =~ m{^Externals$}i;
        next if $name =~ m{^Downloads$}i;
        next if $name =~ m{^Merge$}i;
        if ( $include_archives ) {
            my ( $alt, $rev, $minor_rev, $extension ) = split_entry( $name );
            $names{ $alt } = 1 if ( $extension && defined $rev );
        }
        next unless -d File::Spec->catfile( $path, $name );
        my $addon_info = AddonInfo->lookup( $name );
        next if $addon_info && $addon_info->is_dummy;
        $names{ $name } = 1;
    } ## end foreach my $name ( File::Spec...)
    $dirh->close;
    undef $dirh;
    return sort keys %names;
} ## end sub get_subdirs_in_dir($;$ )

# Get restricted externals
sub get_addon_externals($$ ) {
    my ( $addon_info, $node_path ) = @_;

    my %externals = ();

    return %externals unless $addon_info;
    return %externals unless $node_path;

    if ( defined $opt_actions{ "embedurl" } ) {
        my @urls = keys %force_embed_url;
        $addon_info->add_embed_urls( @urls );
    }
    if ( defined $opt_actions{ "disembedurl" } ) {
        my @urls = keys %unforce_embed_url;
        $addon_info->remove_embed_urls( @urls );
    }
    my %embed_urls  = map { $_ => 1 } $addon_info->get_embed_urls;
    my %unused_urls = %embed_urls;
    my $full_embed  = $addon_info->are_externals_embedded || $node_path->force_externals_embedded;
    unless ( $full_embed || %embed_urls ) {
        return %externals;
    }

    my %try_externals = $node_path->get_externals;
    while ( my ( $path, $url ) = each %try_externals ) {
        my $can_embed;
        my $uri = URI->new( $url )->canonical;
        if ( $embed_urls{ $uri } ) {
            delete $unused_urls{ $uri };
            $can_embed = 1;
        }
        if ( $full_embed || $can_embed ) {
            $externals{ $path } = $url;
        }
    } ## end while ( my ( $path, $url ...))

    $addon_info->remove_embed_urls( keys %unused_urls );

    return %externals;
} ## end sub get_addon_externals($$ )

# path and URI to source type
sub source_type_for($$ ) {
    my ( $path, $url ) = @_;
    my $uri_path;

    if ( defined $url ) {
        $url =~ s{/*$}{};
        my $uri = URI->new( $url )->canonical;
        my $scheme = $uri->scheme || "";
        $uri_path = $uri->path || "";
        if (   $scheme eq "svn"
            || $scheme eq "svn+ssh" )
        {
            return &SOURCE_SVN;
        }
        elsif ($scheme eq "git"
            || $scheme eq "git+ssh" )
        {
            return &SOURCE_GIT;
        }
        elsif ( $uri_path =~ m{\.git$}i ) {
            return &SOURCE_GIT;
        }
    } ## end if ( defined $url )

    if ( defined $path ) {
        if ( -d $path ) {
            if ( -d File::Spec->catdir( $path, &DIR_SVN ) ) {
                return &SOURCE_SVN;
            }
            elsif ( -d File::Spec->catdir( $path, &DIR_GIT ) ) {
                return &SOURCE_GIT;
            }
            elsif ( -d File::Spec->catdir( $path, &DIR_HG ) ) {
                return &SOURCE_HG;
            }
        } ## end if ( -d $path )
        elsif ( -f $path ) {
            my ( $vol, $dir, $fname ) = File::Spec->splitpath( $path );
            my @dirs = File::Spec->splitdir( $dir );
            if ( @dirs && ( uc $dirs[ -1 ] eq uc &DIR_DOWNLOADS ) ) {
                return &SOURCE_DOWNLOAD;
            }
        } ## end elsif ( -f $path )
    } ## end if ( defined $path )

    if ( defined $uri_path && $uri_path =~ m{\bhg\b} ) {
        return &SOURCE_HG;
    }

    return undef;
} ## end sub source_type_for($$ )

# Fetching addons
my %uri_fetch_cache = ();

sub fetch_uri_raw($$$;$$$ ) {
    my ( $name, $path, $url, $source_type, $ignore_mismatch, $auto_force ) = @_;

    $source_type = source_type_for( $path, $url ) unless defined $source_type;
    unless ( defined $source_type ) {
        print STDERR "Assuming: " . $url . " is SVN\n";
        $source_type = &SOURCE_SVN;
    }

    if ( $path =~ m{\.(?:lua|toc|xml)$} ) {

        # We have a single file here instead of a directory
        my ( $vol, $dir, $file ) = File::Spec->splitpath( $path );
        $url .= "";
        $url =~ s{/[^/]*$}{};
        my @dir      = File::Spec->splitdir( $dir );
        my $last_dir = pop @dir;
        $path = File::Spec->catpath( $vol, File::Spec->catdir( @dir ), $last_dir );
    } ## end if ( $path =~ m{\.(?:lua|toc|xml)$})

    if ( $source_type eq &SOURCE_SVN ) {
        return fetch_svn_to_path( $name, $path, $url, $ignore_mismatch, $auto_force );
    }
    elsif ( $source_type eq &SOURCE_GIT ) {
        return fetch_git_to_path( $name, $path, $url, $ignore_mismatch, $auto_force );
    }
    elsif ( $source_type eq &SOURCE_HG ) {
        return fetch_hg_to_path( $name, $path, $url, $ignore_mismatch, $auto_force );
    }
    elsif ( $source_type eq &SOURCE_DOWNLOAD ) {
        return ( 0, undef );
    }

    warn "No idea how to get: " . $name . "\n";
    return ( 0, undef );
} ## end sub fetch_uri_raw($$$;$$$ )

# Fetch addon
my %fetched_addons = ();

sub fetch_addon($ ) {
    my ( $addon_path ) = @_;
    return unless $opt_actions{ "fetch" };
    return unless $addon_path;
    my $key = $addon_path->get_base_key;

    return if $fetched_addons{ $key };
    $fetched_addons{ $key } = 1;

    my $node_path  = $addon_path->get_node_path( "" );
    my $addon      = $addon_path->get_base_name;
    my $addon_info = AddonInfo->lookup( $addon );

    return if $addon_info && $addon_info->is_dummy;

    if ( $addon_info && $addon_info->is_dead ) {
        if ( defined $opt_source{ "svn" } ) {
            $addon_info = $addon_info->clear_dead_as_svn( $opt_source{ "svn" } );
        }
        elsif ( defined $opt_source{ "git" } ) {
            $addon_info = $addon_info->clear_dead_as_git( $opt_source{ "git" } );
        }
        elsif ( defined $opt_source{ "hg" } ) {
            $addon_info = $addon_info->clear_dead_as_hg( $opt_source{ "hg" } );
        }
        else {
            warn "Unable to fetch addon (use a -svn url, -git url or -hg url option): " . $addon . "\n";
            return;
        }
    } ## end if ( $addon_info && $addon_info...)

    # Treat installed / uninstalled as true if any split is installed...
    if (   $opt_filters{ "installed" }
        || $opt_filters{ "uninstalled" } )
    {
        my @check = ( $addon );
        if ( $addon_info && $addon_info->is_split_allowed ) {
            push @check, $addon_info->get_splits;
        }
        my $is_installed;
        while ( @check && !$is_installed ) {
            my $check = shift @check;
            $is_installed = 1 if -d File::Spec->catdir( install_dir,          $check );
            $is_installed = 1 if -d File::Spec->catdir( disabled_install_dir, $check );
        }
        if ( $is_installed ) {
            return unless $opt_filters{ "installed" };
        }
        else {
            return unless $opt_filters{ "uninstalled" };
        }
    } ## end if ( $opt_filters{ "installed"...})

    ### Check some filters
    my $toc = $node_path->get_toc;
    if ( defined $toc ) {
        return undef unless check_toc_filter( $toc );
    }
    my $current_rev = $node_path->get_revision;
    if ( $opt_filters{ "revision" }
        && ( $opt_filters{ "revision-op" } || "" ) ne "" )
    {
        return undef unless do_op_compare_number(
            $opt_filters{ "revision-op" },
            ( $current_rev || 0 ),
            $opt_filters{ "revision" }
        );
    } ## end if ( $opt_filters{ "revision"...})
    if ( !$opt_flags{ "force" } && $opt_filters{ "svn" } ) {
        my $svn_url = $node_path->get_source_url;
        my $val     = format_string(
            $opt_filters{ "svn" },
            "name"   => $addon,
            "branch" => "trunk/"
        );
        my $comp = $opt_filters{ "svn-op" } || "";
        if ( defined $svn_url ) {
            return unless do_op_compare_uri( $comp, $svn_url, $val );
        }
    } ## end if ( !$opt_flags{ "force"...})
    if ( !$opt_flags{ "force" } && $opt_filters{ "git" } ) {
        my $git_url = $node_path->get_source_url;
        my $val     = format_string(
            $opt_filters{ "git" },
            "name"   => $addon,
            "branch" => "trunk/"
        );
        my $comp = $opt_filters{ "git-op" } || "";
        if ( defined $git_url ) {
            return unless do_op_compare_uri( $comp, $git_url, $val );
        }
    } ## end if ( !$opt_flags{ "force"...})
    if ( !$opt_flags{ "force" } && $opt_filters{ "hg" } ) {
        my $hg_url = $node_path->get_source_url;
        my $val    = format_string(
            $opt_filters{ "hg" },
            "name"   => $addon,
            "branch" => "trunk/"
        );
        my $comp = $opt_filters{ "hg-op" } || "";
        if ( defined $hg_url ) {
            return unless do_op_compare_uri( $comp, $hg_url, $val );
        }
    } ## end if ( !$opt_flags{ "force"...})

    ### Register addon if it was not in database
    my $make_assumption = !defined $addon_info;

    unless ( defined $addon_info ) {
        if ( $opt_flags{ "force" } ) {
            if ( defined $opt_source{ "svn" } ) {
                $addon_info = AddonInfo->make_svn_url( $addon, $opt_source{ "svn" } );
            }
            elsif ( defined $opt_source{ "git" } ) {
                $addon_info = AddonInfo->make_git_url( $addon, $opt_source{ "git" } );
            }
            elsif ( defined $opt_source{ "hg" } ) {
                $addon_info = AddonInfo->make_hg_url( $addon, $opt_source{ "hg" } );
            }
        } ## end if ( $opt_flags{ "force"...})
    } ## end unless ( defined $addon_info)
    unless ( defined $addon_info ) {

        # If addon was fetched before use current repository
        my $src_url  = $node_path->get_source_url;
        my $path     = $addon_path->get_real_path;
        my $src_type = defined $src_url && source_type_for( $path, $src_url );

        $addon_info = AddonInfo->make_source_url( $addon, $src_type, $src_url );
    } ## end unless ( defined $addon_info)
    unless ( defined $addon_info ) {
        if ( defined $opt_source{ "svn" } ) {
            $addon_info = AddonInfo->make_svn_url( $addon, $opt_source{ "svn" } );
        }
        elsif ( defined $opt_source{ "git" } ) {
            $addon_info = AddonInfo->make_git_url( $addon, $opt_source{ "git" } );
        }
        elsif ( defined $opt_source{ "hg" } ) {
            $addon_info = AddonInfo->make_hg_url( $addon, $opt_source{ "hg" } );
        }
    } ## end unless ( defined $addon_info)
    unless ( defined $addon_info ) {
        warn "No idea how to fetch addon: " . $addon . "\n";
        return;
    }

    if ( $make_assumption ) {
        $addon_info->make_assumption;
    }

    ### Okay lets actually fetch it
    my $source_type = $addon_info->get_source_type;
    if ( $source_type eq &SOURCE_DOWNLOAD ) {
        return;    # Nothing to do... manually downloaded addon
    }
    if (   $source_type eq &SOURCE_SVN
        || $source_type eq &SOURCE_GIT
        || $source_type eq &SOURCE_HG )
    {
        if ( defined $opt_filters{ "svn-only" } ) {
            if ( $source_type eq &SOURCE_SVN ) {
                return undef unless $opt_filters{ "svn-only" };
            }
            else {
                return undef if $opt_filters{ "svn-only" };
            }
        } ## end if ( defined $opt_filters...)
        my $source_url = $addon_info->get_url( $source_type );

        # Ignore old zip files here...
        my $old_entry = $addon_path->get_entry;
        $addon_path->change_entry( $addon );
        my $path = $addon_path->get_real_path;
        $addon_path->change_entry( $old_entry );

        my $ignore_mismatch = 1;
        undef $ignore_mismatch unless $addon_info->is_assumption;
        undef $ignore_mismatch if ( $opt_source{ $source_type } );

        my $fetch_path = $path;
        my $rel_path;
        if ( $^O ne "MSWin32" && $addon_path->is_stage_dir ) {
            $fetch_path = get_path_for_external( $source_url, $opt_flags{ "dryrun" } );
            my @parent_path = File::Spec->splitdir( $path );
            pop @parent_path;
            my $parent_path = File::Spec->catdir( @parent_path );
            $rel_path = File::Spec->abs2rel( $fetch_path, $parent_path );
            if ( !-e $fetch_path && -d $path ) {
                print_command( "mv", $path, $fetch_path ) if $opt_output{ "verbose" };
                if ( !$opt_flags{ "dryrun" } ) {
                    rename( $path, $fetch_path );
                }
                print_command( "ln", "-s", $fetch_path, $path ) if $opt_output{ "verbose" };
                if ( !$opt_flags{ "dryrun" } ) {
                    symlink( $rel_path, $path );
                }
            } ## end if ( !-e $fetch_path &&...)
        } ## end if ( $^O ne "MSWin32" ...)

        my ( $did_fetch, $can_comment ) = fetch_uri_raw( $addon, $fetch_path, $source_url, $source_type, $ignore_mismatch );
        if ( $opt_flags{ "dryrun" } ) {
            if ( $addon_info->is_assumption ) {
                warn "Would add " . $addon . " to the addon database at url: " . $addon_info->[ AddonInfo::field_url ] . "\n";
            }
            $addon_info->clear_if_assumption;
        } ## end if ( $opt_flags{ "dryrun"...})
        else {
            if ( $fetch_path ne $path ) {
                if ( -l $path ) {
                    if ( readlink( $path ) ne $rel_path ) {
                        print_command( "rm", $path ) if $opt_output{ "verbose" };
                        unlink( $path );
                        print_command( "ln", "-s", $rel_path, $path ) if $opt_output{ "verbose" };
                        symlink( $rel_path, $path );
                    } ## end if ( readlink( $path )...)
                } ## end if ( -l $path )
                else {
                    my $bak_path = $path . ".bak";
                    if ( -e $path ) {
                        rmtree( $bak_path, $opt_output{ "verbose" } ) if -e $bak_path;
                        print_command( "mv", $path, $bak_path ) if ( $opt_output{ "verbose" } );
                        rename( $path, $bak_path );
                    }
                    print_command( "ln", "-s", $rel_path, $path ) if $opt_output{ "verbose" };
                    symlink( $rel_path, $path );
                } ## end else [ if ( -l $path ) ]
            } ## end if ( $fetch_path ne $path)
            if ( $did_fetch ) {
                $addon_info->keep_assumption;

                # Prune zip files here...
                $addon_path->change_entry( $addon );
                $addon_path->prune;
                push @to_comment, $path if ( $can_comment && -d $path );
            } ## end if ( $did_fetch )
            else {
                $addon_info->clear_if_assumption;
            }
        } ## end else [ if ( $opt_flags{ "dryrun"...})]
    } ## end if ( $source_type eq &SOURCE_SVN...)
    else {
        warn "No idea how to fetch addon: " . $addon . "\n";
        return;
    }

    my %externals = get_addon_externals( $addon_info, $node_path );
    my $is_stage_dir = $addon_path->is_stage_dir;
    while ( my ( $path, $url ) = each %externals ) {
        my $real_path;
        if ( 1 || $is_stage_dir ) {
            $real_path = get_path_for_external( $url );
        }
        else {
            $real_path = $addon_path->get_node_path( $path )->get_virtual_path;
        }
        fetch_uri_raw( $addon . ":" . $path, $real_path, $url, undef, undef, $is_stage_dir );
    } ## end while ( my ( $path, $url ...))
} ## end sub fetch_addon($ )

# Find addon externals
my %find_externals_addons = ();

sub find_externals_for_addon($ ) {
    my ( $addon_path ) = @_;

    return unless $addon_path;
    my $key = $addon_path->get_base_key;

    return if $find_externals_addons{ $key };
    $find_externals_addons{ $key } = 1;

    my $node_path    = $addon_path->get_node_path( "" );
    my $addon        = $addon_path->get_base_name;
    my $is_stage_dir = $addon_path->is_stage_dir;
    return unless $is_stage_dir;

    my $addon_info = AddonInfo->lookup( $addon );

    return unless $addon_info;

    my $source_type = $addon_info->get_source_type;
    if (   $source_type eq &SOURCE_SVN
        || $source_type eq &SOURCE_GIT
        || $source_type eq &SOURCE_HG )
    {
        my $source_url = $addon_info->get_url( $source_type );
        my $real_path  = get_path_for_external( $source_url, 1 );
        my $stage_path = $addon_path->get_real_path;
        if ( -l $stage_path && abs_path( $stage_path ) eq abs_path( $real_path ) ) {

            # Okay
            # warn $real_path,"\n";
        }
        else {
            mark_external_path_used( abs_path( $stage_path ) );

            # warn $source_url,"\n","\t",$real_path,"\n";
        }
        mark_external_path_used( $real_path );
    } ## end if ( $source_type eq &SOURCE_SVN...)

    my %externals = get_addon_externals( $addon_info, $node_path );
    while ( my ( $path, $url ) = each %externals ) {
        my $real_path = get_path_for_external( $url );
        mark_external_path_used( $real_path );
    }
} ## end sub find_externals_for_addon($ )

# Read addon tree
sub lua_unescape($ );
sub lua_escape($;$$ );
sub parse_lua_string($ );
sub parse_lua_key($ );
sub parse_lua_locale_assignment($ );
sub parse_lua_locale_table($$ );
sub parse_lua_locale_additions($$ );
sub parse_lua_locale($$ );
sub dump_lua_table($$% );

sub lua_unescape($ ) {
    my $string = shift;
    $string =~ s{\\(\d(?:\d\d?)?|\D)}{
    my $val = $1;
    ($val =~ m{\d})            ? chr($val)
    : exists $lua_escape{$val} ? $lua_escape{$val}
    :                            $val;
  }ge;
    return $string;
} ## end sub lua_unescape($ )

sub lua_escape($;$$ ) {
    my ( $string, $encode, $bracket_space ) = @_;

    if ( $string =~ m{^(nil|false|true|0x[0-9A-Fa-f]+|\d+(?:\.\d+)?(?:[Ee]-?\d+)?)$} ) {
        return $string;
    }
    $string =~ s{^\*}{};
    if ( $encode ) {
        $string =~ s{([\\\"[:^print:]])}{
      $1 eq '"' ? '\\"' : "\\".ord($1);
    }ge;
        return '"' . $string . '"';
    } ## end if ( $encode )
    elsif ( $string =~ m{[\"[:^print:]]} || $string =~ m{\\$} ) {
        my $equal = "=";
        while ( 1 ) {
            my $pattern = "]" . $equal . "]";
            last unless $string =~ m{\Q$pattern\E};
            $equal .= "=";
        }
        my $space = "";
        $space = " " if $bracket_space;
        return $space . '[' . $equal . '[' . $string . ']' . $equal . ']' . $space;
    } ## end elsif ( $string =~ m{[\"[:^print:]]}...)
    else {
        return '"' . $string . '"';
    }
} ## end sub lua_escape($;$$ )

sub parse_lua_string($ ) {
    my ( $ref ) = @_;

    $$ref =~ s{^\s*}{};

    if ( $$ref =~ s{^(nil|false|true|0x[0-9A-Fa-f]+|\d+(?:\.\d+)?(?:[Ee]-?\d+)?)\b}{} ) {
        return $1;
    }
    elsif ( $$ref =~ s{^\"((?:[^\"\\]+|\\.)*)\"}{} ) {
        return "*" . lua_unescape( $1 );
    }
    elsif ( $$ref =~ s{^\'((?:[^\"\\]+|\\.)*)\'}{} ) {
        return "*" . lua_unescape( $1 );
    }
    elsif ( $$ref =~ s{^\[(=*)\[}{} ) {
        my $prefix = $1;
        return undef unless $$ref =~ s{^(.*?)\]$prefix\]}{}s;
        return "*" . $1;
    }
} ## end sub parse_lua_string($ )

sub parse_lua_key($ ) {
    my ( $ref ) = @_;

    $$ref =~ s{^\s*}{};

    if ( $$ref =~ s{^([[:alpha:]_]\w*)}{} ) {
        return $1;
    }
    elsif ( $$ref =~ s{\[}{} ) {
        my $res = parse_lua_string( $ref );
        return undef unless $$ref =~ s{\]}{};
        return $res;
    }
    return undef;
} ## end sub parse_lua_key($ )

sub parse_lua_locale_assignment($ ) {
    my ( $ref ) = @_;

    my $name = parse_lua_key( $ref );
    return undef unless defined $name;
    return undef unless $$ref =~ s{^\s*=\s*}{};
    my $value;
    if ( $$ref =~ m/^{/ ) {
        $value = {};
        return undef unless parse_lua_locale_table( $value, $ref );
    }
    else {
        $value = parse_lua_string( $ref );
        return undef unless $value;
    }
    $name =~ s{^\*}{};
    if ( $$ref =~ s{^(,?)\s*--\s*Needs\s+review\n}{$1\n} ) {
        $value = "?" . $value;
    }
    return ( $name, $value );
} ## end sub parse_lua_locale_assignment($ )

sub parse_lua_locale_table($$ ) {
    my ( $table, $ref ) = @_;

    return undef unless $$ref =~ s/^\s*{\s*//;

    while ( 1 ) {
        return 1 if $$ref =~ s/^}\s*//;
        my $is_comment;
        $is_comment = 1 if $$ref =~ s{^--\s*}{};
        my ( $name, $value ) = parse_lua_locale_assignment( $ref );
        return undef unless defined $name;
        $$ref =~ s{^(?>\s*),?\s*}{};
        if ( $is_comment ) {
            $value = "!" . $value;
        }
        $table->{ $name } = $value;
    } ## end while ( 1 )

    # Not reached
} ## end sub parse_lua_locale_table($$ )

sub parse_lua_locale_additions($$ ) {
    my ( $table, $ref ) = @_;

    while ( 1 ) {
        $$ref =~ s{^\s*}{};
        return 1 if $$ref eq "";

        my $is_comment;
        $is_comment = 1 if $$ref =~ s{^--\s*}{};
        return undef unless $$ref =~ s{^L\s*}{};

        if ( $$ref =~ s{^\.\s*}{} ) {

            # L.name ...
        }
        elsif ( $$ref =~ m{^\[} ) {

            # L[name] ...
        }
        else {
            return undef;
        }

        my ( $name, $value, $review ) = parse_lua_locale_assignment( $ref );
        return undef unless defined $name;
        $$ref =~ s{^\s+}{};
        $$ref =~ s{^,\s*}{};
        $$ref =~ s{^;\s*}{};
        if ( $is_comment ) {
            $value = "!" . $value;
        }
        $table->{ $name } = $value;
    } ## end while ( 1 )
    return undef;
} ## end sub parse_lua_locale_additions($$ )

sub parse_lua_locale($$ ) {
    my ( $table, $ref ) = @_;
    $$ref =~ s{^s*}{};
    if ( $$ref =~ m/^{/ ) {
        unless ( parse_lua_locale_table( $table, $ref ) ) {
            return undef;
        }
        $$ref =~ s{^\s*,}{};
    } ## end if ( $$ref =~ m/^{/ ) (})
    elsif ( $$ref =~ m/^(?:--\s*)?L/ ) {
        unless ( parse_lua_locale_additions( $table, $ref ) ) {
            return undef;
        }
    }
    $$ref =~ s{^\s*}{};
    return undef unless $$ref eq "";

    # Fix up namespaces here...
    if ( none { ref $_ } values %$table ) {
        my @slash_keys = grep { m{/} } keys %$table;
    SPLIT_KEY:
        foreach my $key ( @slash_keys ) {
            my $stable = $table;
            my $value  = $table->{ $key };
            while ( $key =~ s{^([^/]*)/}{} ) {
                $stable = $stable->{ $1 } ||= {};
                next SPLIT_KEY unless ref $stable;
            }
            $stable->{ $key } ||= $value;
        } ## end foreach my $key ( @slash_keys)
    } ## end if ( none { ref $_ } values...)
    return 1;
} ## end sub parse_lua_locale($$ )

sub dump_lua_table($$% ) {
    my ( $table, $en_table, %field ) = @_;

    my $format             = $field{ "format" }               || "lua_additive_table";
    my $handle_unlocalized = $field{ "handle-unlocalized" }   || "blank";
    my $escape             = $field{ "escape-non-ascii" }     || "false";
    my $valprefix          = $field{ "prefix-values" }        || "";
    my $table_name         = $field{ "table-name" }           || "L";
    my $same_key_is_true   = $field{ "same-key-is-true" }     || "false";
    my $namespace          = $field{ "namespace" }            || "";
    my $handle_sub         = $field{ "handle-subnamespaces" } || "none";
    my $delimiter          = $field{ "namespace-delimiter" }  || "/";
    my $prefix             = $field{ "prefix" }               || "";
    my $indent             = $field{ "indent" }               || "";
    my $guarded            = $field{ "guarded" };

    my $empty = {};
    while ( $namespace =~ s{^([^/]+)/*}{} ) {
        $table    = $table->{ $1 }    || $empty;
        $en_table = $en_table->{ $1 } || $empty;
    }
    $table    ||= $empty;
    $en_table ||= $empty;

    my $res   = "";
    my $close = "";
    if ( $format eq "lua_table" ) {
        $res .= "{\n";
        $close = $indent . "}";
        $close .= "\n" if $indent eq "";
        $indent .= "\t";
    } ## end if ( $format eq "lua_table")
    my %both;
    if ( ref $table ) {
        %both = %$table;
    }
    if ( ref $en_table && $table ne $en_table ) {
        %both = ( %both, %$en_table );
    }

    if ( $handle_sub eq "concat" ) {
        my %guarded = %$guarded if $guarded;
        map { $guarded{ $prefix . $_ } = 1 } keys %both;
        $field{ "guarded" } = \%guarded;
    }

DUMP_KEY:
    foreach my $key ( sort keys %both ) {
        my $rkey = $prefix . $key;

        next DUMP_KEY if ( $guarded && $guarded->{ $rkey } );

        my $value      = $table->{ $key };
        my $en_value   = $en_table->{ $key };
        my $key_format = "";
        if ( $format eq "lua_additive_table" ) {
            if ( $rkey =~ m{^[[:alpha:]_]\w+$} ) {
                $key_format = $table_name . "." . $rkey;
            }
            else {
                $key_format = $table_name . "[" . lua_escape( $rkey, $escape ne "false", 1 ) . "]";
            }
        } ## end if ( $format eq "lua_additive_table")
        else {
            if ( $rkey =~ m{^[[:alpha:]_]\w+$} ) {
                $key_format = $rkey;
            }
            else {
                $key_format = "[" . lua_escape( $rkey, $escape ne "false", 1 ) . "]";
            }
        } ## end else [ if ( $format eq "lua_additive_table")]

        if ( ref $value ) {
            if ( $handle_sub eq "subtable" ) {
                $res 
                    .= $indent 
                    . $key_format . " = "
                    . dump_lua_table(
                    $value, $en_value, %field,
                    format => "lua_table",
                    indent => $indent
                    );
                if ( $format eq "lua_table" ) {
                    $res .= ",\n";
                }
            } ## end if ( $handle_sub eq "subtable")
            elsif ( $handle_sub eq "concat" ) {
                $res .= dump_lua_table(
                    $value, $en_value, %field,
                    prefix => $rkey . $delimiter
                );
            } ## end elsif ( $handle_sub eq "concat")
        } ## end if ( ref $value )
        else {
            my $is_comment;
            $is_comment = 1 if defined $value && $value =~ s{^\!}{};
            if ( !defined $value || $is_comment ) {
                if ( $handle_unlocalized eq "english" ) {
                    undef $is_comment;
                    $value      = $en_value;
                    $value      = "!" unless defined $value;
                    $is_comment = 1 if $value =~ s{^\!}{};
                } ## end if ( $handle_unlocalized...)
                elsif ( $handle_unlocalized eq "comment" ) {
                    $value = $en_value;
                    $value =~ s{^\!}{} if defined $value;
                    $is_comment = 1;
                }
                elsif ( $handle_unlocalized eq "ignore" ) {
                    next DUMP_KEY;
                }
                else {
                    $value      = "";
                    $is_comment = 1;
                }
            } ## end if ( !defined $value ||...)
            my $needs_review;
            $needs_review = 1 if defined $value && $value =~ s{^\?}{};
            if ( $same_key_is_true eq "true" && $value eq "*" . $key ) {
                $value = "true";
            }
            $res .= $indent;
            $res .= "-- " if $is_comment;
            $res .= $key_format . " = " . $valprefix . lua_escape( $value, $escape ne "false" );
            if ( $format eq "lua_table" ) {
                $res .= ",";
            }
            if ( $needs_review ) {
                $res .= " -- Needs review";
            }
            $res .= "\n";
        } ## end else [ if ( ref $value ) ]
    } ## end foreach my $key ( sort keys...)
    return $res . $close;
} ## end sub dump_lua_table($$% )

my %url_open = ();

sub open_url_in_browser($ ) {
    my $addr = shift;
    return if exists $url_open{ $addr };
    $url_open{ $addr } = 1;

    my @cmd = ( "open", $addr );
    print_command( @cmd ) if $opt_output{ "verbose" };
    do_command( @cmd );
} ## end sub open_url_in_browser($ )

my %file_open = ();

sub open_file_in_editor($ ) {
    my $fname = shift;
    return if exists $file_open{ $fname };
    $file_open{ $fname } = 1;

    my @cmd = ( "open", "-t", $fname );
    print_command( @cmd ) if $opt_output{ "verbose" };
    do_command( @cmd );
} ## end sub open_file_in_editor($ )

my %local_read = ();
my %local_open = ();

sub read_locale($;$$ ) {
    my ( $fname, $project_time, $version_info ) = @_;

    return $local_read{ $fname } if exists $local_read{ $fname };
    $local_read{ $fname } = undef;
    my $check_url = undef;

    if ( $version_info ) {
        my $url = $version_info->{ &VINFO_URL };
        if ( defined $url && $url =~ m{\.wowace.com/wow/([^/]*)/} ) {
            $check_url = "http://www.wowace.com/addons/$1/localization/export/";
        }
    } ## end if ( $version_info )
    unless ( -f $fname ) {
        if ( $fname =~ m{\.enUS$} ) {
            warn "Locale missing: ", $fname, "\n";
            if ( defined $check_url ) {
                warn "See: ", $check_url, "\n";
            }
            if ( $opt_actions{ "editlocale" } ) {
                my $fh = IO::File->new( $fname, "w" );
                $fh->close;

                open_url_in_browser( $check_url ) if defined $check_url;
                open_file_in_editor( $fname );
            } ## end if ( $opt_actions{ "editlocale"...})
        } ## end if ( $fname =~ m{\.enUS$})
        return undef;
    } ## end unless ( -f $fname )

    my ( $size, $mtime ) = ( stat( $fname ) )[ 7, 9 ];
    my $out_of_date;
    if ( defined $project_time && $mtime < $project_time ) {
        my $age = $project_time - $mtime;
        if ( $age > 86400 ) {
            $out_of_date = 1;
        }
        elsif ( $opt_flags{ "force" } && $age > 0 ) {
            $out_of_date = 1;
        }
    } ## end if ( defined $project_time...)
    if ( $size == 0 ) {
        $out_of_date = 1;
    }
    if ( $out_of_date ) {
        warn "Locale might be out of date: '", $fname, "'\n";
        if ( defined $check_url ) {
            warn "See: ", $check_url, "\n";
        }
        if ( $opt_actions{ "editlocale" } ) {
            open_url_in_browser( $check_url ) if defined $check_url;
            open_file_in_editor( $fname );
        }
    } ## end if ( $out_of_date )
    my $contents = read_file( $fname, binmode => ':raw' );
    return undef unless defined $contents;
    return undef unless $contents =~ m{\S};

    my $parse = {};
    if ( parse_lua_locale( $parse, \$contents ) ) {
        return $local_read{ $fname } = $parse;
    }
    return undef;
} ## end sub read_locale($;$$ )

sub replace_locale($$ ) {
    my ( $parms, $version_info ) = @_;
    my $orig_parms = $parms;
    my %fields     = ();
    while ( $parms =~ s{([^=\s]+)\s*=\s*(?:([^\"\',\s][^,]*[^,\s])|\"([^\"]*)\"|\'([^\']*)\')[\s,]*}{} ) {
        my $field = $1;
        my $val   = (
              defined $2 ? $2
            : defined $3 ? $3
            : $4
        );
        $fields{ $field } = $val;
    } ## end while ( $parms =~ s{([^=\s]+)\s*=\s*(?:([^\"\',\s][^,]*[^,\s])|\"([^\"]*)\"|\'([^\']*)\')[\s,]*})

    return undef if ( $parms ne "" );
    return undef unless exists $fields{ locale };
    return undef unless $fields{ locale } =~ m{^\w*$};
    return undef unless $version_info;
    my $root = $version_info->{ &VINFO_REPO_ROOT };
    return undef unless $root;
    my $lfile = File::Spec->catfile( $root, "locale." . $fields{ locale } );
    my $efile = File::Spec->catfile( $root, "locale.enUS" );
    my $project_time = $version_info->{ &VINFO_PROJECT_TIMESTAMP };
    my $l_locale = read_locale( $lfile, $project_time, $version_info );
    return undef unless defined $l_locale;
    my $e_locale = read_locale( $efile, undef, $version_info );

    if ( defined $fields{ "key" } ) {
        my $key                = $fields{ "key" };
        my $handle_unlocalized = $fields{ "handle-unlocalized" } || "blank";
        my $namespace          = $fields{ "namespace" } || "";

        my $empty    = {};
        my $table    = $l_locale;
        my $en_table = $e_locale;
        while ( $namespace =~ s{^([^/]+)/*}{} ) {
            $table    = $table->{ $1 }    || $empty;
            $en_table = $en_table->{ $1 } || $empty;
        }
        $table    ||= $empty;
        $en_table ||= $empty;
        my $value = $table->{ $key };
        undef $value if ref $value;
        my $en_value = $en_table->{ $key };
        undef $en_value if ref $en_value;
        my $is_comment = undef;

        if ( !defined $value || $value =~ m{^\!} ) {
            if ( $handle_unlocalized eq "english" ) {
                $value = $en_value;
                if ( !defined $value || $value =~ s{^\!}{} ) {
                    $is_comment = 1;
                }
            } ## end if ( $handle_unlocalized...)
            elsif ( $handle_unlocalized eq "comment" ) {
                $value = $en_value;
                $value =~ s{^\!}{} if defined $value;
                $is_comment = 1;
            }
            elsif ( $handle_unlocalized eq "ignore" ) {
                return undef;
            }
            else {
                $value      = "";
                $is_comment = 1;
            }
        } ## end if ( !defined $value ||...)

        return undef if ( $is_comment );
        $value =~ s{^\??\*?}{};
        return $value;
    } ## end if ( defined $fields{ ...})
    else {
        return undef unless exists $fields{ format };
        return dump_lua_table( $l_locale, $e_locale, %fields );
    }
} ## end sub replace_locale($$ )

my %keywords = (
    "file-revision" => {
        keyword => 1,
        func    => sub {
            my $rev = $_[ 0 ] && $_[ 0 ]->{ &VINFO_FILE_REVISION };
            $1 if $rev && $rev =~ m{^(\d+)$};
        },
    },

    "project-revision" => {
        keyword => 1,
        func    => sub {
            my $rev = $_[ 0 ] && $_[ 0 ]->{ &VINFO_PROJECT_REVISION };
            $1 if $rev && $rev =~ m{^(\d+)$};
        },
    },

    "file-hash" => {
        keyword => 1,
        func    => sub { $_[ 0 ] && $_[ 0 ]->{ &VINFO_FILE_HASH }, },
    },

    "project-hash" => {
        keyword => 1,
        func    => sub { $_[ 0 ] && $_[ 0 ]->{ &VINFO_PROJECT_HASH }, },
    },

    "file-abbreviated-hash" => {
        keyword => 1,
        func    => sub {
            my $hash = $_[ 0 ] && $_[ 0 ]->{ &VINFO_FILE_HASH };
            $1 if $hash && $hash =~ m{^(......)};
        },
    },

    "project-abbreviated-hash" => {
        keyword => 1,
        func    => sub {
            my $hash = $_[ 0 ] && $_[ 0 ]->{ &VINFO_PROJECT_HASH };
            $1 if $hash && $hash =~ m{^(......)};
        },
    },

    "file-author" => {
        keyword => 1,
        func    => sub { $_[ 0 ] && $_[ 0 ]->{ &VINFO_FILE_AUTHOR }, },
    },

    "project-author" => {
        keyword => 1,
        func    => sub { $_[ 0 ] && $_[ 0 ]->{ &VINFO_PROJECT_AUTHOR }, },
    },

    "file-date-iso" => {
        keyword => 1,
        func    => sub {
            my $time = $_[ 0 ] && $_[ 0 ]->{ &VINFO_FILE_TIMESTAMP };
            $time && time2str( "%Y-%m-%dT%H:%M:%SZ", $time, "UTC" );
        },
    },

    "project-date-iso" => {
        keyword => 1,
        func    => sub {
            my $time = $_[ 0 ] && $_[ 0 ]->{ &VINFO_PROJECT_TIMESTAMP };
            $time && time2str( "%Y-%m-%dT%H:%M:%SZ", $time, "UTC" );
        },
    },

    "file-date-integer" => {
        keyword => 1,
        func    => sub {
            my $time = $_[ 0 ] && $_[ 0 ]->{ &VINFO_FILE_TIMESTAMP };
            $time && time2str( "%Y%m%d%H%M%S", $time, "UTC" );
        },
    },

    "project-date-integer" => {
        keyword => 1,
        func    => sub {
            my $time = $_[ 0 ] && $_[ 0 ]->{ &VINFO_PROJECT_TIMESTAMP };
            $time && time2str( "%Y%m%d%H%M%S", $time, "UTC" );
        },
    },

    "file-timestamp" => {
        keyword => 1,
        func    => sub { $_[ 0 ] && $_[ 0 ]->{ &VINFO_PROJECT_TIMESTAMP }, },
    },

    "project-timestamp" => {
        keyword => 1,
        func    => sub { $_[ 0 ] && $_[ 0 ]->{ &VINFO_PROJECT_TIMESTAMP }, },
    },

    "project-version" => {
        keyword => 1,
        func    => sub {
            my $rev = $_[ 0 ] && $_[ 0 ]->{ &VINFO_PROJECT_REVISION };
            return "r" . $rev if $rev && $rev =~ m{^\d+$};
            $rev;
        },
    },

    "alpha" => {
        block => 1,
        func  => sub { return 0; },
    },
    "debug" => {
        block => 1,
        func  => sub { return 0; },
    },
    "non-alpha" => {
        block => 1,
        func  => sub { return 1; },
    },
    "non-debug" => {
        block => 1,
        func  => sub { return 1; },
    },
    "no-lib-strip" => {
        block => 1,
        ### FIXME : Broken for .pkgmeta / enable-nolib-creation: no
        ### But then again why would a addon use no-lib-strib if it turned off
        ### no-lib support.
        func => sub { return $_[ 0 ] ? $_[ 0 ]->are_externals_embedded : 1 },
    },
    "do-not-package" => {
        remove => 1,
    },
    "localization" => {
        args => 1,
        func => \&replace_locale,
    },
);

my %replaced_nodes = ();

sub replace_node($;$ ) {
    my ( $fnode, $addon_info ) = @_;

    my $key = $fnode->get_key;
    return unless defined $key;
    return if $replaced_nodes{ $key };
    $replaced_nodes{ $key } = 1;
    my $name   = $fnode->get_name;
    my $is_lua = ( $name =~ m{\.lua$}i );
    my $is_toc = ( $name =~ m{\.toc$}i );
    my $is_xml = ( $name =~ m{\.xml$}i );
    if ( $is_lua || $is_toc || $is_xml ) {

        # ToDo - finishing handling @..@ replacements
        my $contents = $fnode->get_contents;
        my $got_info;
        my $version_info;
        my @res              = ( "" );
        my %active_condition = ();
        my $any_changes      = 0;
        my $start_pos        = 0;
        pos( $contents ) = undef;

        while ( $contents =~ m{(\@((?>end-?+)?)([a-z-]*+)(?>\(\h*+(\V*)\))?\@)}g ) {
            my ( $full, $is_end, $type, $args ) = ( $1, $2, $3, $4 );
            if ( $start_pos ne $-[ 0 ] ) {
                push @res, substr $contents, $start_pos, $-[ 0 ] - $start_pos;
            }
            $start_pos = $+[ 0 ];
            my $keyword = $keywords{ $type };
            my $func = $keyword && $keyword->{ "func" };
            my $replaced;
            if ( !defined $keyword ) {
            }
            elsif ( $keyword->{ "args" } && $func && $args ) {
                $version_info = $fnode->get_version_info unless $got_info;
                $got_info = 1;
                my $val = $func->( $args, $version_info );
                if ( defined $val ) {
                    chomp $val;
                    $res[ -1 ] =~ s{--\h*$}{} if $is_lua;
                    push @res, $val;
                    $replaced = 1;
                } ## end if ( defined $val )
            } ## end elsif ( $keyword->{ "args"...})
            elsif ( $keyword->{ "keyword" } && $func && !$is_end ) {
                $version_info = $fnode->get_version_info unless $got_info;
                $got_info = 1;
                my $val = $func->( $version_info );
                if ( defined $val && $val ne "" ) {
                    push @res, $val;
                    $replaced = 1;
                }
            } ## end elsif ( $keyword->{ "keyword"...})
            elsif ( $keyword->{ "block" } && $func ) {
                my $block_keys = $active_condition{ $type } ||= [];
                if ( $is_end ) {
                    my $index = pop @$block_keys;
                    if ( defined $index ) {
                        my $un_comment = $func->( $addon_info );
                        if ( $is_lua ) {
                            $res[ $index - 1 ] =~ s{\[===\[\h*$}{};
                            $res[ $index - 1 ] .= "[===[" unless $un_comment;
                            scalar( $contents =~ m{(?:\h*\]===\])?}g );
                            $start_pos = $+[ 0 ];
                            push @res, $full;
                            push @res, "]===]" unless $un_comment;
                            $replaced = 1;
                        } ## end if ( $is_lua )
                        elsif ( $is_xml ) {
                            $res[ $index + 1 ] =~ s{^\h*-->}{};
                            $res[ $index + 1 ] = "-->" . $res[ $index + 1 ] if $un_comment;
                            $res[ -1 ] =~ s{<\!--\h*$}{};
                            $res[ -1 ] .= "<!--" if $un_comment;
                            push @res, $full;
                            $replaced = 1;
                        } ## end elsif ( $is_xml )
                        elsif ( $is_toc ) {
                            unless ( $un_comment ) {
                                for ( my $idx = $index + 1 ; $idx < scalar @res ; ++$idx ) {
                                    $res[ $idx ] =~ s{^}{#}gm;
                                }
                                $res[ $index + 1 ] =~ s{^#}{};
                                $res[ -1 ] =~ s{#\h*$}{};
                            } ## end unless ( $un_comment )
                            push @res, $full;
                            $replaced = 1;
                        } ## end elsif ( $is_toc )
                    } ## end if ( defined $index )
                } ## end if ( $is_end )
                else {
                    push @$block_keys, scalar @res;
                }
            } ## end elsif ( $keyword->{ "block"...})
            elsif ( $keyword->{ "remove" } ) {
                my $block_keys = $active_condition{ $type } ||= [];
                if ( $is_end ) {
                    my $index = pop @$block_keys;
                    for ( my $idx = $index ; $idx < scalar @res ; ++$idx ) {
                        $res[ $idx ] = "";
                    }
                    if ( $is_lua ) {
                        $res[ $index - 1 ] =~ s{--\h*$}{};
                    }
                    elsif ( $is_xml ) {
                        $res[ $index - 1 ] =~ s{<\!--\h*$}{};
                        scalar( $contents =~ m{(?:\h*-->)?}g );
                        $start_pos = $+[ 0 ];
                    }
                    elsif ( $is_toc ) {
                        $res[ $index - 1 ] =~ s{#\h*$}{};
                    }
                    $replaced = 1;
                } ## end if ( $is_end )
                else {
                    push @$block_keys, scalar @res;
                }
            } ## end elsif ( $keyword->{ "remove"...})
            if ( $replaced ) {
                $any_changes = 1;
            }
            else {
                push @res, $full;
            }
        } ## end while ( $contents =~ m{(\@((?>end-?+)?)([a-z-]*+)(?>\(\h*+(\V*)\))?\@)}g)
        if ( $any_changes ) {
            push @res, substr $contents, $start_pos;
            $fnode->replace_contents( join( "", @res ) );

            # Key just changed on us with content changed.
            my $key = $fnode->get_key;
            $replaced_nodes{ $key } = 1;
        } ## end if ( $any_changes )
    } ## end if ( $is_lua || $is_toc...)
} ## end sub replace_node($;$ )

sub get_pkgmeta_node_for_path($$ ) {
    my ( $pkgmeta_info, $path ) = @_;

    my $pkgmeta_node = $pkgmeta_info;
    $pkgmeta_node->{ "path" } //= File::Spec->curdir;
    foreach my $name ( File::Spec->splitdir( $path ) ) {
        next if ( $name eq File::Spec->curdir );
        if ( $name eq File::Spec->updir ) {
            ### Limit .. to current tree
            $pkgmeta_node = ( $pkgmeta_node->{ "parent" } || $pkgmeta_info );
        }
        else {
            my $key    = uc $name;
            my $parent = $pkgmeta_node;
            $pkgmeta_node = $pkgmeta_node->{ "dirs" } ||= {};
            $pkgmeta_node = $pkgmeta_node->{ $key }   ||= {};
            $pkgmeta_node->{ "name" }   = $name;
            $pkgmeta_node->{ "key" }    = $key;
            $pkgmeta_node->{ "parent" } = $parent;
            $pkgmeta_node->{ "path" } //= File::Spec->catdir( $parent->{ "path" }, $name );
        } ## end else [ if ( $name eq File::Spec...)]
    } ## end foreach my $name ( File::Spec...)

    return $pkgmeta_node;
} ## end sub get_pkgmeta_node_for_path($$ )

sub read_addon($$$ ) {
    my ( $ref_addon_table, $node_path, $adjust_files ) = @_;

    return unless $node_path;
    return unless $node_path->get_key;

    my %externals;
    my $pkgmeta_info;

    my $node   = AddonSubdirectory->new_root( $node_path, $ref_addon_table );
    my %addons = ();                                                            # { uc $name } = [ $node, $pkgmeta ];
    my @moves  = ();                                                            # [ uc $addon, $path, $pkgmeta, $source ];

    if ( !$node_path->get_addon_path->is_install_dir ) {
        if ( $node->get_info ) {
            %externals = get_addon_externals( $node->get_info, $node_path );
            if ( %externals ) {
                $pkgmeta_info ||= {};
                while ( my ( $ext_path, $ext_url ) = each %externals ) {
                    my $pkgmeta_node = get_pkgmeta_node_for_path( $pkgmeta_info, $ext_path );
                    my $ext_path = get_path_for_external( $ext_url, 1 );
                    my $source_node;
                    if ( -f $ext_path ) {
                        my ( $vol, $dir, $file ) = File::Spec->splitpath( $ext_path );
                        $source_node = $node_path->get_addon_path->same_base_for( File::Spec->catpath( $vol, $dir, "" ) )->get_node_path( $file );
                        $pkgmeta_node->{ "is_file" } = 1;
                    }
                    else {
                        $source_node = $node_path->get_addon_path->same_base_for( $ext_path )->get_node_path( "" );
                    }
                    $pkgmeta_node->{ "source" }     = $source_node;
                    $pkgmeta_node->{ "ignore_sub" } = 1;
                } ## end while ( my ( $ext_path, $ext_url...))
            } ## end if ( %externals )
        } ## end if ( $node->get_info )
        my %move_folders = ( $node_path->get_move_folders );
        if ( %move_folders ) {
            $pkgmeta_info ||= {};
            while ( my ( $move_path, $move_name ) = each %move_folders ) {
                my $pkgmeta_node = get_pkgmeta_node_for_path( $pkgmeta_info, $move_path );
                my $source_node  = $node_path->get_subnode_path( $pkgmeta_node->{ "path" } );
                my @move_dirs    = File::Spec->splitdir( $move_name );
                my $subaddon;
                do {
                    $subaddon = shift @move_dirs;
                } while ( @move_dirs && !File::Spec->no_upwards( $subaddon ) );
                if ( @move_dirs ) {

                    # Non-split move -- defer til later since we do not yet know all the splits yet
                    push @moves, [ uc $subaddon, File::Spec->catdir( @move_dirs ), $pkgmeta_node, $source_node ];
                }
                else {
                    if ( $node->get_info ) {
                        my $addon_info = $node->get_info;
                        $addon_info->add_split( $subaddon );
                        if ( $addon_info->is_split_allowed ) {
                            $pkgmeta_node->{ "split" }      = 1;
                            $pkgmeta_node->{ "ignore_sub" } = 1;
                            my $snode_path = $node_path->get_subnode_path( $pkgmeta_node->{ "path" } );
                            my $snode = $node->new_subroot( $snode_path, $ref_addon_table, $subaddon );
                            $addons{ uc $subaddon } = [ $snode, $pkgmeta_node ];
                        } ## end if ( $addon_info->is_split_allowed)
                    } ## end if ( $node->get_info )
                } ## end else [ if ( @move_dirs ) ]
            } ## end while ( my ( $move_path, ...))
        } ## end if ( %move_folders )
        foreach my $ignore_path ( $node_path->get_ignored ) {
            $pkgmeta_info ||= {};
            my $pkgmeta_node = get_pkgmeta_node_for_path( $pkgmeta_info, $ignore_path );
            $pkgmeta_node->{ "ignore" }     = 1;
            $pkgmeta_node->{ "ignore_sub" } = 1;
        } ## end foreach my $ignore_path ( $node_path...)
    } ## end if ( !$node_path->get_addon_path...)

    # Add main addon
    my $ucname = uc $node->get_addon_name;
    $addons{ $ucname } = [ $node, $pkgmeta_info ];

    # Special smarts for ACE3
    if ( $ucname eq "ACE3" && $node->get_info ) {
        my ( $dirs, $files ) = $node_path->ls;
        my $addon_info = $node->get_info;

        $pkgmeta_info ||= {};
        while ( my ( $name, $dnode ) = each %$dirs ) {
            if ( $name =~ m{-\d+\.\d+$} ) {
                my $subaddon = $dnode->get_name;

                my $pkgmeta_node = get_pkgmeta_node_for_path( $pkgmeta_info, $subaddon );
                $addon_info->add_split( $subaddon );
                if ( $addon_info->is_split_allowed ) {
                    $pkgmeta_node->{ "split" }      = 1;
                    $pkgmeta_node->{ "ignore_sub" } = 1;
                    $pkgmeta_node->{ "ace3" }       = $node;
                    my $snode = $node->new_subroot( $dnode, $ref_addon_table, $subaddon );
                    $addons{ uc $subaddon } = [ $snode, $pkgmeta_node ];
                } ## end if ( $addon_info->is_split_allowed)
            } ## end if ( $name =~ m{-\d+\.\d+$})
        } ## end while ( my ( $name, $dnode...))
    } ## end if ( $ucname eq "ACE3"...)

    # Handle non-split moves
    foreach my $move ( @moves ) {
        my ( $ucaddon, $target_path, $src_pkgmeta, $src_node ) = @$move;

        my $addon_rec = $addons{ $ucaddon };
        next unless defined $addon_rec;

        my ( $split_node, $split_pkgmeta ) = @$addon_rec;

        my $tgt_pkgmeta = get_pkgmeta_node_for_path( $split_pkgmeta, $target_path );

        if ( $tgt_pkgmeta != $src_pkgmeta ) {
            $tgt_pkgmeta->{ "source" }     = $src_node;
            $tgt_pkgmeta->{ "ignore_sub" } = 1;
            $src_pkgmeta->{ "ignore" }     = 1;
            $src_pkgmeta->{ "ignore_sub" } = 1;
            if ( $src_node->is_file ) {
                $tgt_pkgmeta->{ "is_file" } = 1;
            }
        } ## end if ( $tgt_pkgmeta != $src_pkgmeta)
    } ## end foreach my $move ( @moves )

    # Main addon
    $node = read_addon_node( $ref_addon_table, $node, $adjust_files, $pkgmeta_info );

    if ( $node && !$node->is_valid ) {
        delete $ref_addon_table->{ $node->get_addon_name };
        print "Filtered: " . $node->get_addon_name . "\n" if $opt_output{ "verbose" };
        undef $node;
    }

    # Split addons
    foreach my $ucaddon ( sort keys %addons ) {
        my $addon_rec = $addons{ $ucaddon };
        my ( $split_node, $split_pkgmeta ) = @$addon_rec;
        next if $split_node == $node;

        $split_node = read_addon_node( $ref_addon_table, $split_node, $adjust_files, $split_pkgmeta );

        if ( $split_node && !$split_node->is_valid ) {
            delete $ref_addon_table->{ $split_node->get_addon_name };
            print "Filtered: " . $node->get_addon_name . "\n" if $opt_output{ "verbose" };
        }
    } ## end foreach my $ucaddon ( sort ...)

    if ( $node && $adjust_files ) {
        my $addon_tocinfo = $node->get_toc;
        while ( my ( $ext_path, $ext_url ) = each %externals ) {
            my $ext_node = $node->find_dir( $ext_path );
            my $ext_node_path;
            if ( $ext_node ) {
                $ext_node_path = $ext_node->get_node_path;
            }
            else {
                my ( $ext_file, $ext_file_node ) = ( $node->find_path( $ext_path ) );
                if ( ref $ext_file ) {
                    $ext_node_path = $ext_file->[ 1 ];
                }
            } ## end else [ if ( $ext_node ) ]
            if ( $ext_node_path ) {
                my ( $rev, $last_rev ) = $ext_node_path->get_revision;
                if ( defined $last_rev ) {
                    $addon_tocinfo->set_embed_rev( $ext_path, $last_rev ) if $addon_tocinfo;
                }
            } ## end if ( $ext_node_path )
        } ## end while ( my ( $ext_path, $ext_url...))
    } ## end if ( $node && $adjust_files)

    return $node;
} ## end sub read_addon($$$ )

sub read_addon_node($$$$;$ ) {
    my ( $ref_addon_table, $node, $adjust_files, $pkgmeta_node, $ignore ) = @_;

    #print "Reading: ".$node->get_addon_name."\n" if $opt_output{"verbose"} && $node->is_root;

    my $node_path = $node->get_node_path;
    return unless ( $pkgmeta_node || $node_path->get_key );

    my $ucaddon     = uc $node->get_addon_name;
    my $ucnode_name = uc $node->get_dir_name;

    my $pkgmeta_node_dirs = $pkgmeta_node && $pkgmeta_node->{ "dirs" };
    my $pkgmeta_node_move = $pkgmeta_node && $pkgmeta_node->{ "move" };
    my $ace_library       = $pkgmeta_node && $pkgmeta_node->{ "ace3" };

    my $addon_tocinfo;
    my $addon_has_contents = undef;

    my $dirs;
    my $files;
    if ( $ignore ) {
        $dirs  = {};
        $files = {};
    }
    else {
        ( $dirs, $files ) = $node_path->ls;
    }

    if ( $node->is_root ) {
        #### Look for possible TOC files
        #### Also handle stand alone libraries without .toc files
        my $addon_tocnode;
        my %toc_nodes = ();
        my $check_for_lib = ( $node->is_root && $ucaddon =~ m{^LIB} );
        my $library_source;
        my $uclibrary_source;
        my $suffix_toc_len  = length( &SUFFIX_TOC );
        my $uc_suffix_toc   = uc &SUFFIX_TOC;
        my %uclibrary_names = (
            $ucaddon . q{.LUA} => 1,
            $ucaddon . q{.XML} => 2,
        );
        my $new_files = {};

        while ( my ( $name, $fnode ) = each %$files ) {
            if ( uc substr( $name, -$suffix_toc_len, $suffix_toc_len ) eq $uc_suffix_toc ) {
                my $shortname = substr( $name, 0, -$suffix_toc_len );
                my $ucname = uc $shortname;
                $toc_nodes{ $ucname } = $fnode;
            }
            else {

                # Build a list of files without TOC nodes.
                $new_files->{ $name } = $fnode;
            }
            if ( $check_for_lib ) {
                my $ucelem = uc $name;
                if ( $uclibrary_names{ $ucelem } ) {
                    if ( !defined $library_source || $uclibrary_names{ $ucelem } ) {
                        $library_source   = $name;
                        $uclibrary_source = $ucelem;
                    }
                } ## end if ( $uclibrary_names{...})
            } ## end if ( $check_for_lib )
        } ## end while ( my ( $name, $fnode...))

        # Future uses of files will not see any TOC nodes.
        $files = $new_files;

        my @toc_nodes = values %toc_nodes;
        if ( $toc_nodes{ $ucaddon } ) {
            $addon_tocnode = $toc_nodes{ $ucaddon };
        }
        elsif ( $toc_nodes{ $ucnode_name } ) {
            $addon_tocnode = $toc_nodes{ $ucnode_name };
        }
        elsif ( 1 == scalar @toc_nodes ) {
            $addon_tocnode = $toc_nodes[ 0 ];
        }
        elsif ( scalar @toc_nodes ) {
            warn "Not sure which node to use for: ", $node->get_addon_name, "\n";
        }
        elsif ( $ace_library ) {
            my $ace_toc = $ace_library->get_toc;
            $ace_toc->set_tag( &TOC_OPTIONAL_DEPENDENCIES, "LibStub", "AceAddon-3.0", "AceConsole-3.0", "AceConfig-3.0" );

            my $gen_toc      = get_temp_path( "gen_toc" );
            my $toc_filename = $node->get_addon_name . &SUFFIX_TOC;
            my $toc_file     = File::Spec->catdir( $gen_toc, $toc_filename );
            $addon_tocnode = AddonPath->lookup( $gen_toc )->get_node_path( $toc_filename );
            my $fh = new IO::File $toc_file, "w";
            print $fh "##Interface: ", ( $ace_toc->get_tag( &TOC_INTERFACE ) || "40000" ), "\n";
            print $fh "##Title: Lib: ", $node->get_addon_name, "\n";
            print $fh "##OptionalDeps: LibStub\n"
                unless ( $ucnode_name eq "LIBSTUB" );
            print $fh $node->get_addon_name, ".xml\n";
            close $fh;
        } ## end elsif ( $ace_library )
        elsif ( defined $library_source ) {
            my $gen_toc      = get_temp_path( "gen_toc" );
            my $toc_filename = $node->get_addon_name . &SUFFIX_TOC;
            my $toc_file     = File::Spec->catdir( $gen_toc, $toc_filename );
            $addon_tocnode = AddonPath->lookup( $gen_toc )->get_node_path( $toc_filename );
            my $fh = new IO::File $toc_file, "w";
            my $libname = $node->get_addon_name;
            $libname =~ s{^lib}{}i;
            my $wow_version = wow_version;

            if ( defined $wow_version ) {
                $wow_version =~ s{\d$}{0};
            }
            print $fh "## Interface: " . ( $wow_version || "40000" ) . "\n";
            print $fh "## Title: Lib: " . $libname . "\n";
            print $fh "## OptionalDeps: LibStub\n";
            my $version_info = $node_path->get_version_info;
            if ( $version_info ) {
                my $ver = $version_info->{ &VINFO_PROJECT_REVISION };
                if ( $ver ) {
                    if ( $ver =~ m{^\d+$} ) {
                        $ver = "r" . $ver;
                    }
                    print $fh "## Version: " . $ver . "\n";
                } ## end if ( $ver )
                my $author = $version_info->{ &VINFO_PROJECT_AUTHOR };
                if ( $author ) {
                    print $fh "## Author: " . $author . "\n";
                }
            } ## end if ( $version_info )
            print $fh "## X-Category: Library\n";

            # Can we get authors from repository?
            print $fh $library_source . "\n";
            close $fh;
        } ## end elsif ( defined $library_source)

        if ( $addon_tocnode ) {
            $addon_has_contents = 1;
            replace_node( $addon_tocnode, $node->get_info ) if ( $adjust_files );
            $node->set_toc( $addon_tocinfo = TOCInfo->load_from_path( $addon_tocnode ) );
        }

        if ( $addon_tocinfo
            && ( $addon_tocinfo->get_tag( &TOC_CATEGORY ) || "" ) eq "Library" )
        {
            my $addon_info = $node->get_info;
            foreach my $key ( $addon_tocinfo->get_tags ) {
                if ( $key =~ m{^X-AceLibrary-(.*)} ) {
                    $addon_info->add_libname( $1 ) if $addon_info;
                }
            }
        } ## end if ( $addon_tocinfo &&...)
    } ## end if ( $node->is_root )

    # Load subdirectories into node
    # TODO Consider excluding the libraries where newer versions are already installed separately.
    my $node_dirs = $node->get_dirs;
    while ( my ( $name, $dnode ) = each %$dirs ) {
        my $key = uc $name;

        next if ( $key eq &DIR_UC_SVN );
        next if ( $key eq &DIR_UC_GIT );
        next if ( $key eq &DIR_UC_HG );

        my $snode;
        my $pkgmeta_subnode = $pkgmeta_node_dirs && $pkgmeta_node_dirs->{ $key };
        next if ( $pkgmeta_subnode && $pkgmeta_subnode->{ "ignore_sub" } );

        $snode = $node->new_subdir( $dnode );
        read_addon_node( $ref_addon_table, $snode, $adjust_files, $pkgmeta_subnode, $ignore );
        if ( $snode->is_valid ) {
            $addon_has_contents = 1 unless ( $key eq &DIR_UC_SVN
                || $key eq &DIR_UC_GIT
                || $key eq &DIR_UC_HG );
        }
        else {
            delete $node_dirs->{ $key };
        }
    } ## end while ( my ( $name, $dnode...))

    # Load rest of embeds into node
    if ( $pkgmeta_node_dirs ) {
        while ( my ( $key, $pkgmeta_subnode ) = each %$pkgmeta_node_dirs ) {
            my $name = $pkgmeta_subnode->{ "name" };
            my $snode;
            next if $pkgmeta_subnode->{ "split" };
            my $sub_ignore = $ignore;
            if ( $pkgmeta_subnode->{ "source" } ) {
                undef $sub_ignore;
                my $source_node = $pkgmeta_subnode->{ "source" };
                if ( $pkgmeta_subnode->{ "is_file" } ) {
                    my $node_files = $node->get_files;
                    my $key        = uc $source_node->get_name;
                    $addon_has_contents = 1;
                    $node_files->{ $key } = [ $name, $source_node ];
                    next;
                } ## end if ( $pkgmeta_subnode->...)
                else {
                    $snode = $node->new_subdir( $source_node, $pkgmeta_subnode->{ "name" } );
                    $pkgmeta_subnode = undef;
                }
            } ## end if ( $pkgmeta_subnode->...)
            elsif ( $pkgmeta_subnode->{ "dirs" } ) {
                my $dname = $node_path->get_subnode_path( $name );
                $snode = $node->new_subdir( $dname );
            }
            else {
                if ( $pkgmeta_subnode->{ "ignore" } ) {
                    my $node_files = $node->get_files;
                }
                next;
            } ## end else [ if ( $pkgmeta_subnode->...)]
            read_addon_node( $ref_addon_table, $snode, $adjust_files, $pkgmeta_subnode, $sub_ignore );
            if ( $snode->is_valid ) {
                $addon_has_contents = 1 unless ( $key eq &DIR_UC_SVN
                    || $key eq &DIR_UC_GIT
                    || $key eq &DIR_UC_HG );
            }
            else {
                delete $node_dirs->{ $key };
            }
        } ## end while ( my ( $key, $pkgmeta_subnode...))
    } ## end if ( $pkgmeta_node_dirs)

    # Load files into node
    my $node_files = $node->get_files;
    while ( my ( $name, $fnode ) = each %$files ) {
        my $key = uc $name;

        my $pkgmeta_subnode = $pkgmeta_node_dirs && $pkgmeta_node_dirs->{ $key };
        next if ( $pkgmeta_subnode && $pkgmeta_subnode->{ "ignore_sub" } );
        next if ( $pkgmeta_subnode && exists $node_files->{ $key } );

        $node_files->{ $key } = [ $name, $fnode ];
        $addon_has_contents = 1;
        replace_node( $fnode, $node->get_info ) if ( $adjust_files );
    } ## end while ( my ( $name, $fnode...))

    # Special case for SharedMedia
    if (   $ucaddon eq "SHAREDMEDIA"
        && $node->is_root
        && find_command( "ftxinstalledfonts" )
        && 0 )
    {

        # Find template
        my $template;
        my $media_node;
        if ( $node_files->{ "MYMEDIA.LUA" } ) {
            $media_node = $node_files->{ "MYMEDIA.LUA" }->[ 1 ];
            $template = $media_node && $media_node->get_contents;
        }
        elsif ( $node_files->{ "INSTRUCTIONS FOR MYMEDIA.TXT" } ) {
            my $node = $node_files->{ "INSTRUCTIONS FOR MYMEDIA.TXT" }->[ 1 ];
            $template = $node && $node->get_contents;
        }
        $template = "" unless defined $template;
        my @lines = split( /\n/, $template );

        my %replace;

        # Get list of available fonts
        my $fonts_in = new IO::Pipe;
        $fonts_in->reader( find_command( "ftxinstalledfonts" ), "-pl" );

        my $snode  = undef;
        my $sfiles = undef;
        while ( <$fonts_in> ) {
            chomp;
            my ( $name, $path ) = ( split( /\t/, $_, 4 ) )[ 2, 3 ];
            my ( $vol, $dir, $base ) = File::Spec->splitpath( $path );
            next unless $base && $base =~ m{\.[to]tf$};
            my $pdir = AddonPath->lookup( File::Spec->catpath( $vol, $dir, "" ) );
            unless ( $snode ) {
                $snode                        = $node->new_subdir( $pdir->get_node_path( "" ), "systemfonts" );
                $sfiles                       = $snode->get_files;
                $node_dirs->{ "systemfonts" } = $snode;
            }
            $sfiles->{ uc $base } = [ $base, $pdir->get_node_path( $base ) ];
            $snode->make_valid;
            $replace{ $name } = "SharedMedia:Register(\"font\", \"" . $name . "\", [[Interface\\AddOns\\SharedMedia\\systemfonts\\" . $base . "]])\n";
        } ## end while ( <$fonts_in> )
        if ( %replace ) {
            my $new_contents = "";
            while ( @lines ) {
                my $line = shift @lines;
                $new_contents .= $line . "\n";
                last if $line =~ m{^--font:};
            }
            foreach my $font ( sort keys %replace ) {
                $new_contents .= $replace{ $font };
            }
            $new_contents .= join( "\n", @lines, "" );

            unless ( $media_node ) {
                my $gen_mymedia = get_temp_path( "gen_mymedia" );
                my $mymedia     = File::Spec->catdir( $gen_mymedia, "MyMedia.lua" );
                my $fh          = new IO::File $mymedia, "w";
                close $fh;
                $media_node = AddonPath->lookup( $gen_mymedia )->get_node_path( "MyMedia.lua" );
            } ## end unless ( $media_node )
            $media_node->replace_contents( $new_contents );
            $node_files->{ "MYMEDIA.LUA" } = [ "MyMedia.lua", $media_node ];
            $addon_has_contents = 1;
        } ## end if ( %replace )
    } ## end if ( $ucaddon eq "SHAREDMEDIA"...)

    $node->make_valid if ( $addon_has_contents );

    if ( $adjust_files && $addon_tocinfo ) {
        my $interface     = $addon_tocinfo->get_tag( &TOC_INTERFACE );
        my $min_interface = $interface;
        my $max_interface = $addon_tocinfo->get_tag( &TOC_MAX_INTERFACE );
        my $wow_version   = wow_version;
        if ( defined $min_interface && defined $max_interface && defined $wow_version ) {
            my $min_wow_version = $wow_version;
            $min_wow_version =~ s{\d\d$}{00};
            if ( $max_interface < $min_interface ) {
                ( $max_interface, $min_interface ) = ( $min_interface, $max_interface );
            }
            my $best_interface = (
                  $min_interface > $min_wow_version ? $min_interface
                : $max_interface < $min_wow_version ? $max_interface
                : $min_wow_version
            );
            if ( $best_interface != $interface ) {
                $addon_tocinfo->set_tag( &TOC_INTERFACE, $best_interface );
            }
        } ## end if ( defined $min_interface...)

        $addon_tocinfo->set_tag( &TOC_PACKAGE_MANAGER, "addonpkg" );
        if ( $node->get_parent ) {
            $addon_tocinfo->set_tag( &TOC_ADDONPKG_PARENT, $node->get_parent );
        }
        my $rev = ( $node_path->get_revision )[ 1 ];
        if ( $rev ) {
            $addon_tocinfo->set_tag( &TOC_ADDONPKG_REVISION, $rev );
        }
        my $url = $node_path->get_source_url;
        if ( $url ) {
            $addon_tocinfo->set_tag( &TOC_ADDONPKG_URL, $url );
        }
        my @files      = $addon_tocinfo->get_files;
        my $addon_info = $node->get_info;
        foreach my $file ( @files ) {
            unless ( $has_xml_expat ) {
                if (   $file eq "modules.xml"
                    && $addon_info
                    && $addon_info->is_split_allowed )
                {
                    $addon_tocinfo->disable_file( $file );
                    next;
                } ## end if ( $file eq "modules.xml"...)
                if (   $file eq "embeds.xml"
                    && $addon_info
                    && !$addon_info->are_externals_embedded
                    && !$node_path->force_externals_embedded )
                {
                    $addon_tocinfo->disable_file( $file );
                    next;
                } ## end if ( $file eq "embeds.xml"...)
            } ## end unless ( $has_xml_expat )
            unless ( $node->need_path( $file ) ) {
                $addon_tocinfo->disable_file( $file );
                next;
            }
        } ## end foreach my $file ( @files )
    } ## end if ( $adjust_files && ...)
    return $node;
} ## end sub read_addon_node($$$$;$ )

my %read_staged_addons = ();

sub read_staged_addon($ ) {
    my $node_path = shift;
    my @res       = ();

    my $addon      = $node_path->get_name;
    my $addon_info = AddonInfo->lookup( $addon );
    $addon = $addon_info->get_name if $addon_info;

    # Handle installed / uninstalled...
    if (   $opt_filters{ "installed" }
        || $opt_filters{ "uninstalled" } )
    {
        my @check = ( $addon );
        if ( $addon_info && $addon_info->is_split_allowed ) {
            push @check, $addon_info->get_splits;
        }
        my $is_installed;
        while ( @check && !$is_installed ) {
            my $check = shift @check;
            $is_installed = 1 if -d File::Spec->catdir( install_dir,          $check );
            $is_installed = 1 if -d File::Spec->catdir( disabled_install_dir, $check );
        }
        if ( $is_installed ) {
            return @res unless $opt_filters{ "installed" };
        }
        else {
            return @res unless $opt_filters{ "uninstalled" };
        }
    } ## end if ( $opt_filters{ "installed"...})

    return @res if $addon_info && $addon_info->is_dummy;
    return @res if $read_staged_addons{ $addon };
    $read_staged_addons{ $addon } = 1;

    if ( $opt_source{ "libs" } ) {
        my $toc = $node_path->get_toc( $addon . &SUFFIX_TOC );
        if ( $toc ) {
            my $addon_path = $node_path->get_addon_path;
            unless ( $addon_info && ( $addon_info->are_externals_embedded || $node_path->force_externals_embedded ) ) {
                push @res, $addon_path->same_base_for( expand_addon_names( $toc->get_tag( &TOC_EMBEDS ) ) );
            }
            push @res, $addon_path->same_base_for( expand_addon_names( $toc->get_tag( &TOC_REQUIRED_DEPENDENCIES ) ) );
        } ## end if ( $toc )
        if ( $addon_info ) {
            my $addon_path = $node_path->get_addon_path;
            push @res, $addon_path->same_base_for( expand_addon_names( $addon_info->expand_require() ) );
        }
    } ## end if ( $opt_source{ "libs"...})

    if ( $need_read_staged_addon ) {
        my $toc = $node_path->get_toc( $addon . &SUFFIX_TOC );
        return @res unless ( check_toc_filter( $toc ) );
        if ( defined $opt_filters{ "svn-only" } ) {
            my $orig = $node_path->is_in_svn      ? 1 : 0;
            my $val  = $opt_filters{ "svn-only" } ? 1 : 0;
            return @res unless ( $orig == $val );
        }
        my $current_rev = $node_path->get_revision;
        if ( $opt_filters{ "revision" } ) {
            return @res unless $current_rev;
            return @res unless do_op_compare_number(
                ( $opt_filters{ "revision-op" } || "" ),
                $current_rev,
                $opt_filters{ "revision" }
            );
        } ## end if ( $opt_filters{ "revision"...})
        if ( $opt_filters{ "svn" } ) {
            my $svn_url = $node_path->get_source_url;
            my $val     = format_string(
                $opt_filters{ "svn" },
                "name"   => $addon,
                "branch" => "trunk/"
            );
            my $comp = $opt_filters{ "svn-op" } || "";
            return @res unless do_op_compare_uri( $comp, $svn_url, $val );
        } ## end if ( $opt_filters{ "svn"...})
        if ( $opt_filters{ "git" } ) {
            my $git_url = $node_path->get_source_url;
            my $val     = format_string(
                $opt_filters{ "git" },
                "name"   => $addon,
                "branch" => "trunk/"
            );
            my $comp = $opt_filters{ "git-op" } || "";
            return @res unless do_op_compare_uri( $comp, $git_url, $val );
        } ## end if ( $opt_filters{ "git"...})
        if ( $opt_filters{ "hg" } ) {
            my $hg_url = $node_path->get_source_url;
            my $val    = format_string(
                $opt_filters{ "hg" },
                "name"   => $addon,
                "branch" => "trunk/"
            );
            my $comp = $opt_filters{ "hg-op" } || "";
            return @res unless do_op_compare_uri( $comp, $hg_url, $val );
        } ## end if ( $opt_filters{ "hg"...})
    } ## end if ( $need_read_staged_addon)

    if ( defined $opt_actions{ "setgroup" } ) {
        my $addon_path = $node_path->get_addon_path;
        if ( $addon_path->is_stage_dir ) {
            AddonInfo::set_group( $addon, $opt_actions{ "setgroup" } );
            $addon_path->get_real_path;    # Move it if necessary
        }
    } ## end if ( defined $opt_actions...)
    $given_addon_table_all->{ $addon } = 1;

    if ( $need_read_staged_addon ) {
        read_addon( $src_addon_table_all, $node_path, 1 );
    }

    return @res;
} ## end sub read_staged_addon($ )

my %read_staged_addon_paths = ();

sub read_staged_addon_path($;$ ) {
    my ( $addon_path, $raw_name ) = @_;
    my @res = ();
    return @res unless $addon_path;

    my $base_name = $addon_path->get_base_name;
    $raw_name ||= $base_name;

    my $key = File::Spec->catfile( $addon_path->get_base_key, $raw_name );

    return @res if $read_staged_addon_paths{ $key };
    $read_staged_addon_paths{ $key } = 1;

    ### Okay first we fetch...
    fetch_addon( $addon_path ) if ( $opt_actions{ "fetch" } );

    my @node_paths = $addon_path->get_subaddon_nodes;
    my @include    = ();
    foreach my $node_path ( @node_paths ) {
        push @res, read_staged_addon( $node_path );

        my $addon      = $node_path->get_name;
        my $addon_info = AddonInfo->lookup( $addon );
        $addon = $addon_info->get_name if $addon_info;

        push @include, $addon;
    } ## end foreach my $node_path ( @node_paths)

    while ( @include ) {
        my $addon = shift @include;
        if ( $given_addon_table_all->{ $addon }
            && !$given_addon_table->{ $addon } )
        {
            $given_addon_table->{ $addon } = 1;

            if ( $opt_actions{ "enable" } ) {
                enable_addon( $addon );
                my $real_path = File::Spec->catdir( install_dir, $addon );
                my $addon_path = AddonPath->lookup( $real_path, install_dir );
                $addon_path->enable_dir;
            } ## end if ( $opt_actions{ "enable"...})
            if ( $opt_actions{ "disable" } ) {
                if ( $opt_flags{ "force" } ) {
                    my $real_path = File::Spec->catdir( install_dir, $addon );
                    my $addon_path = AddonPath->lookup( $real_path, install_dir );
                    $addon_path->disable_dir;
                }
                else {
                    disable_addon( $addon );
                }
            } ## end if ( $opt_actions{ "disable"...})

            AddonInfo::embed_externals( $addon ) if ( defined $opt_actions{ "embed" } );
            AddonInfo::set_externals_embedded( $addon, "default" ) if ( defined $opt_actions{ "defembed" } );
            AddonInfo::disembed_externals( $addon ) if ( defined $opt_actions{ "disembed" } );

            AddonInfo::allow_split( $addon ) if ( defined $opt_actions{ "split" } );
            AddonInfo::set_split_allowed( $addon, "default" ) if ( defined $opt_actions{ "defsplit" } );
            AddonInfo::disallow_split( $addon ) if ( defined $opt_actions{ "nosplit" } );

            if ( defined $opt_actions{ "embedurl" } ) {
                my @urls = keys %force_embed_url;
                AddonInfo::add_embed_urls( $addon, @urls );
            }
            if ( defined $opt_actions{ "disembedurl" } ) {
                my @urls = keys %unforce_embed_url;
                AddonInfo::remove_embed_urls( $addon, @urls );
            }
        } ## end if ( $given_addon_table_all...)
        my $rec = $src_addon_table_all->{ $addon };
        if ( $rec
            && !$src_addon_table->{ $addon } )
        {
            $src_addon_table->{ $addon } = $rec;
            push @include, $rec->get_children;

            if ( $opt_actions{ "enable" } ) {
                enable_addon( $addon );
                my $real_path = File::Spec->catdir( install_dir, $addon );
                my $addon_path = AddonPath->lookup( $real_path, install_dir );
                $addon_path->enable_dir;
            } ## end if ( $opt_actions{ "enable"...})
            if ( $opt_actions{ "disable" } ) {
                if ( $opt_flags{ "force" } ) {
                    my $real_path = File::Spec->catdir( install_dir, $addon );
                    my $addon_path = AddonPath->lookup( $real_path, install_dir );
                    $addon_path->disable_dir;
                }
                else {
                    disable_addon( $addon );
                }
            } ## end if ( $opt_actions{ "disable"...})
        } ## end if ( $rec && !$src_addon_table...)
    } ## end while ( @include )

    return @res;
} ## end sub read_staged_addon_path($;$ )

my %read_installed_addons = ();

sub read_installed_addon($ ) {
    my $src = shift;

    my $addon      = path_to_node( $src );
    my $addon_info = AddonInfo->lookup( $addon );
    $addon = $addon_info->get_name if $addon_info;

    return if $read_installed_addons{ $addon };
    $read_installed_addons{ $addon } = 1;

    return if $addon_info && $addon_info->is_dummy;

    my $real_path = File::Spec->catdir( install_dir, $addon );
    my $addon_path = AddonPath->lookup( $real_path, install_dir );
    my $node_path = $addon_path->get_node_path( "" );

    read_addon( $dest_addon_table, $node_path, undef );
} ## end sub read_installed_addon($ )

# Dump tree
sub dump_addon($$ ) {
    my ( $ref_addon_table, $addon ) = @_;

    return dump_addon_node( "", $ref_addon_table->{ $addon } );
}

sub dump_addon_node($$ ) {
    my ( $prefix, $node ) = @_;
    my @lines = ();

    return @lines unless ref $node;

    my $node_path    = $node->get_node_path;
    my $toc          = $node->get_toc;
    my $version_info = $node_path->get_version_info;
    my $dirs         = $node->get_dirs;
    my @dirs         = ref $dirs ? ( sort keys %$dirs ) : ();
    my $files        = $node->get_files;
    my @files        = ref $files ? ( sort keys %$files ) : ();
    push @lines, $prefix . "+= PATH: " . $node_path->get_virtual_path . "\n";
    if ( $node->get_parent ) {
        push @lines, $prefix . "+= PARENT: " . $node->get_parent;
    }
    my $bar = ( ( @dirs || @files || ref $version_info ) ? "|" : " " );
    if ( ref $toc ) {
        push @lines, $prefix . "+= TOC\n";
        my $sprefix = $prefix . $bar . "   ";
        foreach my $field ( sort cmp_caseless $toc->get_tags ) {
            my @value = $toc->get_tag( $field );
            my $value;
            if ( @value ) {
                $value = join( ", ", @value );
            }
            else {
                $value = "<undef>";
            }
            $value = "<nil>" if $value eq "";
            push @lines, $sprefix . "+= " . $field . ": " . $value . "\n";
        } ## end foreach my $field ( sort cmp_caseless...)
    } ## end if ( ref $toc )
    $bar = ( ( @dirs || @files ) ? "|" : " " );
    if ( ref $version_info ) {
        push @lines, $prefix . "+= Version Info\n";
        my $sprefix = $prefix . $bar . "   ";
        foreach my $field ( sort cmp_caseless keys %$version_info ) {
            my $value = $version_info->{ $field };
            $value = join( " & ", @$value ) if ( ref $value );
            $value = "<nil>" if $value eq "";
            push @lines, $sprefix . "+= " . $field . ": " . $value . "\n";
        } ## end foreach my $field ( sort cmp_caseless...)
    } ## end if ( ref $version_info)
    $bar = ( ( @files ) ? "|" : " " );
    if ( @dirs ) {
        push @lines, $prefix . "+= DIRS\n";
        my $sprefix = $prefix;
        $sprefix .= $bar . "   ";
        while ( @dirs ) {
            my $dir      = shift @dirs;
            my $dir_name = $dirs->{ $dir }->get_dir_name;
            my $lprefix  = $sprefix . ( @dirs ? "|" : " " ) . "   ";
            if (   $dir eq &DIR_UC_SVN
                || $dir eq &DIR_UC_GIT
                || $dir eq &DIR_UC_HG )
            {
                push @lines, $sprefix . "+= " . $dir_name . " ...\n";
            } ## end if ( $dir eq &DIR_UC_SVN...)
            else {
                push @lines, $sprefix . "+= " . $dir_name . "\n";
                push @lines, dump_addon_node( $lprefix, $dirs->{ $dir } );
            }
        } ## end while ( @dirs )
    } ## end if ( @dirs )
    if ( @files ) {
        push @lines, $prefix . "+= FILES\n";
        while ( @files ) {
            my $file      = shift @files;
            my $file_name = $files->{ $file }->[ 0 ];
            if ( 1 ) {
                push @lines, $prefix . "    += " . $file_name . "\n";
            }
            else {
                my $node  = $files->{ $file }->[ 1 ];
                my $apath = $node->get_addon_path->get_real_path;
                my $rel   = $node->get_rel_dir;
                push @lines, $prefix . "    += " . $file_name . " (" . $apath . ":" . $rel . ")\n";
            } ## end else [ if ( 1 ) ]
        } ## end while ( @files )
    } ## end if ( @files )
    return @lines;
} ## end sub dump_addon_node($$ )

# Install tree
sub install_addon($$$;$$ ) {
    my ( $ref_addon_table, $base_path, $addon, $quiet, $update_only ) = @_;

    my $source_node       = $ref_addon_table->{ $addon };
    my $install_path      = AddonPath->lookup( $addon, $base_path );
    my $install_is_update = -d $install_path->get_real_path;
    my $install_kind      = ( $install_is_update ? "Updat" : "Install" );
    my $install_node      = $install_path->get_node_path( "" );
    my $note              = "";

    if ( $update_only ) {
        return undef unless $install_is_update;
    }

    if ( $install_path->is_disabled_dir ) {
        $note .= " (Disabled)";
    }
    else {
        my ( $enabled, $disabled, $default ) = get_addon_enabled_disabled_default_count( $addon );
        if ( $default ) {
            my $toc = $source_node->get_toc;
            my $toc_state = $toc && $toc->get_tag( "DefaultState" );
            if ( $toc_state && $toc_state =~ m{disabled}i ) {
                $disabled += $default;
            }
            else {
                $enabled += $default;
            }
        } ## end if ( $default )
        if ( $disabled ) {
            $note .= " (" . ( $enabled ? &STATE_MIXABLED : &STATE_DISABLED ) . ")";
        }
    } ## end else [ if ( $install_path->is_disabled_dir)]

    unless ( $quiet ) {
        print $install_kind. "ing: " . $addon . $note . "\n" if $opt_output{ "verbose" };
    }
    my $res = install_addon_node( $install_node, $source_node );
    if ( $res ) {
        my $real_path = $install_path->get_real_path;
        push @to_comment, $real_path if -d $real_path;
        unless ( $quiet ) {
            print $install_kind. "ed: " . $addon . $note . "\n" unless ( $opt_output{ "verbose" } || $opt_flags{ "dryrun" } );
        }
    } ## end if ( $res )
    return $res;
} ## end sub install_addon($$$;$$ )

sub install_addon_node($$ ) {
    my ( $install_node, $node ) = @_;

    my $any_changes;

    die "Internal error missing node.\n" unless ref $node;
    my $node_path = $node->get_node_path;
    die "Internal error node to install to path: " . $install_node->get_virtual_path . "\n" unless defined $node_path;
    my $dirs         = $node->get_dirs;
    my @dirs         = ref $dirs ? ( sort keys %$dirs ) : ();
    my $files        = $node->get_files;
    my @files        = ref $files ? ( sort keys %$files ) : ();
    my %existing     = ();
    my $newest_mtime = undef;

    # Note what files and directories currently exist
    my $install_path = $install_node->get_virtual_path;
    if ( -d $install_path ) {
        my $dirh 
            = IO::Dir->new( $install_path )
            or $opt_flags{ "dryrun" }
            or die "Unable to read: " . $install_path . ": " . $! . "\n";
        if ( defined $dirh ) {
            foreach my $name ( File::Spec->no_upwards( $dirh->read ) ) {
                my $fname = File::Spec->catfile( $install_path, $name );
                next if ( $finder_special_names{ uc $name } );
                $existing{ uc $name } = $fname;
            }
            $dirh->close
                or die "Unable to read: " . $install_path . ": " . $! . "\n";
            undef $dirh;
        } ## end if ( defined $dirh )
    } ## end if ( -d $install_path )
    else {

        # Create directory
        print "mkdir -p " . $install_path . "\n" if ( $opt_output{ "verbose" } && $opt_flags{ "dryrun" } );
        mkpath( $install_path, $opt_output{ "verbose" }, 0777 ) unless $opt_flags{ "dryrun" };
        $any_changes = 1;
    } ## end else [ if ( -d $install_path )]

    # Install files
    foreach my $file ( @files ) {
        my $old = $existing{ $file };
        delete $existing{ $file };
        my $file_rec          = $files->{ $file };
        my $file_node         = $file_rec->[ 1 ];
        my $file_name         = $file_rec->[ 0 ];
        my $file_vpath        = $file_node->get_virtual_path;
        my $file_install_node = $install_node->get_subnode_path( $file_name );
        my $fname             = $file_install_node->get_virtual_path;
        my $name_case_change;
        $name_case_change = 1 if ( $old && $old ne $fname );
        next if $finder_special_names{ $file };
        die "Internal error error: " . $file_vpath . " is not a file!\n" unless $file_node->is_file;

        if ( $name_case_change || ( -l $fname ) || ( !-e $fname ) || $file_node->diff_from( $file_install_node, 1 ) ) {
            rmtree( $fname, $opt_output{ "verbose" } ) if ( -e $fname && !$opt_flags{ "dryrun" } );
            print "cp -f " . $file_vpath . " " . $fname . "\n" if $opt_output{ "verbose" };
            unless ( $opt_flags{ "dryrun" } ) {
                $file_node->copy_to( $file_install_node );
            }
            $any_changes = 1;
        } ## end if ( $name_case_change...)
        my $mtime = ( stat( $fname ) )[ 9 ];
        $newest_mtime = $mtime if ( defined $mtime && ( !defined $newest_mtime || $mtime > $newest_mtime ) );
    } ## end foreach my $file ( @files )

    # Install TOC
    my $toc = $node->get_toc;
    my $toc_fname;
    if ( defined $toc ) {
        my $file_name = $toc->get_name;
        my $file      = uc $file_name;
        my $old       = $existing{ $file };
        delete $existing{ $file };
        my $fname = File::Spec->catfile( $install_path, $file_name );
        $toc_fname = $fname;
        my $name_case_change;
        $name_case_change = 1 if ( $old && $old ne $fname );
        my $old_toc  = TOCInfo->load( $fname );
        my $old_text = $old_toc ? $old_toc->extract : "";
        my $new_text = $toc->extract;

        if ( $name_case_change || ( -l $fname ) || ( !-e $fname ) || ( $old_text ne $new_text ) ) {
            rmtree( $fname, $opt_output{ "verbose" } ) if ( -e $fname && !$opt_flags{ "dryrun" } );
            print "generate " . $fname . "\n" if $opt_output{ "verbose" };
            unless ( $opt_flags{ "dryrun" } ) {
                my $fh = new IO::File $fname, "w";
                print $fh $toc->extract;
                $fh->close
                    or die "Internal error writing: " . $fname . ": $!\n";
            } ## end unless ( $opt_flags{ "dryrun"...})
            $any_changes = 1;
        } ## end if ( $name_case_change...)
    } ## end if ( defined $toc )

    # Install nodes
    foreach my $dir ( @dirs ) {

        # If we start copying these directories below
        # change detection logic should be updated.
        next if ( $dir eq &DIR_UC_SVN );
        next if ( $dir eq &DIR_UC_GIT );
        next if ( $dir eq &DIR_UC_HG );
        my $old = $existing{ $dir };
        delete $existing{ $dir };
        my $dir_name = $dirs->{ $dir }->get_dir_name;
        my $dname = File::Spec->catdir( $install_path, $dir_name );
        if ( -d $dname ) {

            if ( $old && $old ne $dname ) {
                print "mv " . $old . " " . $dname . "\n" if $opt_output{ "verbose" };
                if ( !$opt_flags{ "dryrun" } ) {
                    rename( $old, $dname );
                }
            } ## end if ( $old && $old ne $dname)
        } ## end if ( -d $dname )
        my ( $sub_changes, $mtime ) = install_addon_node( $install_node->get_subnode_path( $dir_name ), $dirs->{ $dir } );
        if ( defined $mtime ) {
            $newest_mtime = $mtime if ( $mtime > ( $newest_mtime || 0 ) );
        }

        # No need to restrict based on .svn, .git, or .hg dir here
        # since now handled above...
        $any_changes = 1 if $sub_changes;
    } ## end foreach my $dir ( @dirs )

    # Remove extra files
    foreach my $file ( values %existing ) {
        $any_changes = 1;
        if ( $opt_flags{ "dryrun" } ) {
            print "rm -rf " . $file . "\n" if $opt_output{ "verbose" };
        }
        else {
            rmtree( $file, $opt_output{ "verbose" } );
        }
    } ## end foreach my $file ( values %existing)

    # TODO Update records if we already read this addon
    if (   0
        && $any_changes
        && $node->is_root )
    {
        my $addon_name = $node->get_addon_name;
        if ( $dest_addon_table->{ $addon_name } ) {
            delete $dest_addon_table->{ $addon_name };
            read_addon( $dest_addon_table, $install_path, undef );
        }
    } ## end if ( 0 && $any_changes...)

    unless ( $opt_flags{ "dryrun" } ) {
        if ( defined $newest_mtime ) {
            utime( $newest_mtime, $newest_mtime, $toc_fname ) if $toc_fname;
            utime( $newest_mtime, $newest_mtime, $install_path );
        }
    } ## end unless ( $opt_flags{ "dryrun"...})

    return $any_changes unless wantarray;
    return ( $any_changes, $newest_mtime );
} ## end sub install_addon_node($$ )

# Reinstall tree
sub reinstall_addon($$$;$ ) {
    my ( $ref_addon_table, $base_path, $addon, $quiet ) = @_;
    return install_addon( $ref_addon_table, $base_path, $addon, $quiet, 1 );
}

# Uninstall tree
sub uninstall_addon($$$;$ ) {
    my ( $ref_addon_table, $base_path, $addon, $quiet ) = @_;

    unless ( $quiet ) {
        print "Uninstalling: " . $addon . "\n" if $opt_output{ "verbose" };
    }
    my $addon_path = File::Spec->catdir( $base_path, $addon );
    my $res = undef;
    if ( -d $addon_path ) {
        if ( $opt_flags{ "dryrun" } ) {
            print "rm -rf " . $addon_path . "\n" if $opt_output{ "verbose" };
        }
        else {
            rmtree( $addon_path, $opt_output{ "verbose" } );
            unless ( $quiet ) {
                print "Uninstalled: " . $addon . "\n" unless $opt_output{ "verbose" };
            }
            $res = 1;
        } ## end else [ if ( $opt_flags{ "dryrun"...})]
    } ## end if ( -d $addon_path )
    delete $ref_addon_table->{ $addon };
    return $res;
} ## end sub uninstall_addon($$$;$ )

# Compare tree
sub compare_addon($$$$;$ ) {
    my ( $src_addon_table, $dest_addon_table, $addon, $check_only, $only_once ) = @_;

    return compare_addon_nodes( $src_addon_table->{ $addon }, $dest_addon_table->{ $addon }, $check_only, $only_once );
}

sub compare_addon_nodes($$$;$ ) {
    my ( $src_node, $dest_node, $check_only, $only_once ) = @_;

    my $src_node_path   = $src_node       && $src_node->get_node_path;
    my $src_node_vpath  = $src_node_path  && $src_node_path->get_virtual_path;
    my $dest_node_path  = $dest_node      && $dest_node->get_node_path;
    my $dest_node_vpath = $dest_node_path && $dest_node_path->get_virtual_path;
    my @changes         = ();

    # First check if both nodes exists...
    unless ( defined $src_node_path ) {
        push @changes, diff_only_in( $dest_node_vpath ) if ( defined $dest_node_vpath );
        return @changes;
    }
    unless ( defined $dest_node_path ) {
        push @changes, diff_only_in( $src_node_vpath );
        return @changes;
    }

    # Second check if only_once is set and file was seen
    if ( ref $only_once ) {
        if (   defined $only_once->{ $src_node_vpath }
            || defined $only_once->{ $dest_node_vpath } )
        {
            if (   $only_once->{ $src_node_vpath }
                || $only_once->{ $dest_node_vpath } )
            {
                push @changes, "Likely changes for " . $src_node_vpath . " and " . $dest_node_vpath . "\n";
                $only_once->{ $src_node_vpath }  = 1;
                $only_once->{ $dest_node_vpath } = 1;
            } ## end if ( $only_once->{ $src_node_vpath...})
            return @changes;
        } ## end if ( defined $only_once...)
        $only_once->{ $src_node_vpath }  = 1;
        $only_once->{ $dest_node_vpath } = 1;
    } ## end if ( ref $only_once )

    # Get unified list of files and directories to check...
    my $src_dirs   = $src_node->get_dirs;
    my $src_files  = $src_node->get_files;
    my $dest_dirs  = $dest_node->get_dirs;
    my $dest_files = $dest_node->get_files;
    my %children   = ( %$src_dirs, %$src_files, %$dest_dirs, %$dest_files );

    # Handle special change log files
    my @src_change_log  = grep { m{^Changelog-.*-r\d+.txt$}i } keys %$src_files;
    my @dest_change_log = grep { m{^Changelog-.*-r\d+.txt$}i } keys %$dest_files;
    if (   1 == scalar @src_change_log
        && 1 == scalar @dest_change_log )
    {
        my $src_change_log  = shift @src_change_log;
        my $dest_change_log = shift @dest_change_log;
        delete $children{ $src_change_log };
        delete $children{ $dest_change_log };
        my $src_log  = $src_files->{ $src_change_log };
        my $dest_log = $dest_files->{ $dest_change_log };
        push @changes, $src_log->[ 1 ]->diff_from( $dest_log->[ 1 ] );
    } ## end if ( 1 == scalar @src_change_log...)

    foreach my $child ( sort keys %children ) {
        next if $child eq &DIR_UC_SVN;
        next if $child eq &DIR_UC_GIT;
        next if $child eq &DIR_UC_HG;
        next if $child =~ m{~$};
        next if $finder_special_names{ $child };

        my $src_dir      = $src_dirs->{ $child };
        my $src_dir_path = $src_dir && $src_dir->get_node_path->get_virtual_path;
        my $src_file     = $src_files->{ $child };
        $src_file = $src_file->[ 1 ] if $src_file;
        my $dest_dir      = $dest_dirs->{ $child };
        my $dest_dir_path = $dest_dir && $dest_dir->get_node_path->get_virtual_path;
        my $dest_file     = $dest_files->{ $child };
        $dest_file = $dest_file->[ 1 ] if $dest_file;

        if ( defined $src_dir ) {
            if ( defined $dest_dir ) {
                push @changes, compare_addon_nodes( $src_dir, $dest_dir, $check_only, $only_once );
            }
            elsif ( defined $dest_file ) {
                push @changes, "File " . $dest_file . " is a regular file while file " . $src_dir_path . " is a directory.\n";
            }
            else {
                push @changes, diff_only_in( $src_dir_path );
            }
        } ## end if ( defined $src_dir )
        elsif ( defined $src_file ) {
            if ( defined $dest_dir ) {
                push @changes, "File " . $dest_dir_path . " is a directory while file " . $src_file . " is a regular file.\n";
            }
            elsif ( defined $dest_file ) {
                push @changes, $src_file->diff_from( $dest_file );
            }
            else {
                push @changes, diff_only_in( $src_file->get_virtual_path );
            }
        } ## end elsif ( defined $src_file)
        else {
            if ( defined $dest_dir ) {
                push @changes, diff_only_in( $dest_dir_path );
            }
            elsif ( defined $dest_file ) {
                push @changes, diff_only_in( $dest_file->get_virtual_path );
            }
            else {
                die "Internal error. Missing nodes!\n";
            }
        } ## end else [ if ( defined $src_dir )]
        return @changes if @changes && $check_only;
    } ## end foreach my $child ( sort keys...)

    # Compare TOC
    my $src_toc       = $src_node       && $src_node->get_toc;
    my $dest_toc      = $dest_node      && $dest_node->get_toc;
    my $src_toc_path  = $src_node_path  && $src_node_path->get_virtual_path;
    my $dest_toc_path = $dest_node_path && $dest_node_path->get_virtual_path;
    if ( defined $src_toc ) {
        if ( defined $dest_toc ) {
            push @changes, diff_toc( $dest_toc, $dest_toc_path, $src_toc, $src_toc_path, $check_only );
        }
        else {
            push @changes, diff_only_in( $src_toc_path );
        }
    } ## end if ( defined $src_toc )
    elsif ( defined $dest_toc ) {
        push @changes, diff_only_in( $dest_toc_path );
    }

    if ( ref $only_once ) {
        unless ( @changes ) {
            $only_once->{ $src_node_vpath }  = 0;
            $only_once->{ $dest_node_vpath } = 0;
        }
    } ## end if ( ref $only_once )

    return @changes;
} ## end sub compare_addon_nodes($$$;$ )

# Log addon
sub generic_get_log($$$$ ) {
    my ( $addon_info, $src_node, $src_rev, $dest_rev ) = @_;

    my $src_path = $src_node->get_virtual_path;
    my $src_url  = $src_node->get_source_url;
    my $source_type;
    if ( $addon_info ) {
        $source_type = $addon_info->get_source_type;
    }
    else {
        $source_type = source_type_for( $src_path, $src_url );
    }
    return () unless defined $source_type;

    if ( $source_type eq &SOURCE_SVN ) {
        return () unless $src_url;
        $src_url =~ s{/$}{};
        return get_svn_log( $src_url, $src_rev, $dest_rev );
    }
    elsif ( $source_type eq &SOURCE_GIT ) {
        return get_git_log( $src_path, $src_rev, $dest_rev );
    }
    elsif ( $source_type eq &SOURCE_HG ) {
        return get_hg_log( $src_path, $src_rev, $dest_rev );
    }
    elsif ( $source_type eq &SOURCE_DOWNLOAD ) {

        # Consider looking for a log file in the addon itself
        return ();
    }
    return ();
} ## end sub generic_get_log($$$$ )

sub log_addon($$$ ) {
    my ( $src_addon_table, $dest_addon_table, $addon ) = @_;

    my @res       = ();
    my $src_addon = $src_addon_table->{ $addon };
    return undef unless defined $src_addon;
    my $src_node_path = $src_addon->get_node_path;
    return undef unless defined $src_node_path;
    my $src_rev = ( $src_node_path->get_revision )[ 0 ];
    return undef unless defined $src_rev;

    my $dest_addon     = $dest_addon_table->{ $addon };
    my $dest_node_path = $dest_addon && $dest_addon->get_node_path;
    my $dest_rev       = $dest_node_path && ( $dest_node_path->get_revision )[ 1 ];
    $dest_rev = 1 unless $dest_rev;

    my $addon_info = AddonInfo->lookup( $addon );
    return @res unless $addon_info;

    push @res, generic_get_log( $addon_info, $src_node_path, $src_rev, $dest_rev );

    my %externals = get_addon_externals( $addon_info, $src_node_path );
    my $dest_toc = $dest_addon && $dest_addon->get_toc;
    foreach my $path ( sort keys %externals ) {
        my $url          = $externals{ $path };
        my $src_ext_node = $src_addon->find_dir( $path );
        my $src_ext_path = $src_ext_node && $src_ext_node->get_node_path;
        my $src_ext_rev  = $src_ext_path && ( $src_ext_path->get_revision )[ 0 ];
        if ( $src_ext_rev ) {
            my $dest_ext_node = $dest_addon    && $dest_addon->find_dir( $path );
            my $dest_ext_path = $dest_ext_node && $dest_ext_node->get_node_path;
            my $dest_ext_rev  = $dest_ext_path && ( $dest_ext_path->get_revision )[ 0 ];
            unless ( $dest_ext_rev ) {
                $dest_ext_rev = $dest_toc->get_embed_rev( $path ) if $dest_toc;
            }
            if ( $dest_ext_rev ) {
                if ( $dest_ext_rev ne $src_ext_rev ) {
                    push @res, "\n", AddonDiff::ColorMeta . "Embed: " . $path . AddonDiff::ColorReset . "\n";
                    push @res, generic_get_log( undef, $src_ext_path, $src_ext_rev, $dest_ext_rev );
                }
            } ## end if ( $dest_ext_rev )
            else {
                push @res, "\n", AddonDiff::ColorMeta . "Added Embed: " . $path . AddonDiff::ColorReset . "\n";
            }
        } ## end if ( $src_ext_rev )
    } ## end foreach my $path ( sort keys...)
    return @res;
} ## end sub log_addon($$$ )

# Output
my $figlet;

sub figlet(@ ) {
    $figlet = find_command( "figlet" ) unless ( defined $figlet );
    my $text = join( " ", @_ );
    my @default = ( $text . "\n", ( '-' x length( $text ) ) . "\n" );
    if ( defined $figlet ) {
        my @output = ();
        my $figin  = new IO::Pipe;
        $figin->reader( $figlet, "-w", $opt_output{ "columns" }, "-f", $opt_output{ "font" }, @_ )
            or return @default;
        while ( <$figin> ) {
            push @output, $_;
        }
        $figin->close
            or return @default;
        return @output;
    } ## end if ( defined $figlet )
    return @default;
} ## end sub figlet(@ )

my @output_header = ();

sub next_output_header(@ ) {
    @output_header = @_;
}

my $less_out;
my @pending_out  = ();
my $first_header = 1;

sub output(@ ) {
    foreach ( @_ ) {
        confess "Undef" unless defined $_;
    }

    if ( @_ && @output_header ) {
        push @pending_out, "\n" unless $first_header;
        undef $first_header;
        push @pending_out, figlet( @output_header );
        @output_header = ();
    } ## end if ( @_ && @output_header)
    push @pending_out, @_;
    if ( $out_is_term && $opt_output{ "lines" } ) {
        unless ( defined $less_out ) {
            if ( !defined $less_out && $opt_output{ "lines" } && ( @pending_out >= $opt_output{ "lines" } ) ) {
                my $less = find_command( "less" ) || find_command( "more" );
                if ( defined $less ) {
                    my @cmd = ( $less );
                    if ( $less =~ m{more\.com$}i && $^O eq "MSWin32" ) {
                        push @cmd, "/E";
                    }
                    elsif ( find_command( "colordiff" ) ) {
                        push @cmd, "-R";
                    }
                    $less_out = new IO::Pipe;
                    $less_out->writer( @cmd );
                } ## end if ( defined $less )
            } ## end if ( !defined $less_out...)
        } ## end unless ( defined $less_out)
        if ( $less_out ) {
            print $less_out @pending_out;
            @pending_out = ();
        }
    } ## end if ( $out_is_term && $opt_output...)
    else {
        print @pending_out;
        @pending_out = ();
    }
} ## end sub output(@ )

sub finish_output( ) {
    if ( $less_out ) {
        print $less_out @pending_out;
        @pending_out = ();
        $less_out->close;
        undef $less_out;
    } ## end if ( $less_out )
    else {
        print @pending_out;
        @pending_out = ();
    }
    @output_header = ();
    $first_header  = 1;
} ## end sub finish_output( )

END { finish_output; }

my %file_by_character = ();
my $character_lists_read;

sub get_character_lists( ) {
    return if $character_lists_read;
    $character_lists_read = 1;

    return unless accounts_dir;

    # Determine accounts
    my $accounts_dirh = new IO::Dir( accounts_dir );
    return unless $accounts_dirh;
    foreach my $account ( File::Spec->no_upwards( $accounts_dirh->read ) ) {
        next if ( $account =~ m{^SavedVariables}i );
        my $account_dir = File::Spec->catdir( accounts_dir, $account );
        next unless -d $account_dir;

        # Determine realms
        my $account_dirh = new IO::Dir( $account_dir );
        next unless $account_dirh;
        foreach my $realm ( File::Spec->no_upwards( $account_dirh->read ) ) {
            next if ( $realm =~ m{^SavedVariables}i );
            my $realm_dir = File::Spec->catdir( $account_dir, $realm );
            next unless -d $realm_dir;

            # Determine characters
            my $realm_dirh = new IO::Dir( $realm_dir );
            next unless $realm_dirh;
            foreach my $character ( File::Spec->no_upwards( $realm_dirh->read ) ) {
                next if ( $character =~ m{^SavedVariables}i );
                my $character_dir = File::Spec->catdir( $realm_dir, $character );
                next unless -d $character_dir;
                my $file = File::Spec->catfile( $character_dir, "AddOns.txt" );

                my $char_path = File::Spec->catdir( $realm, $character );
                my $unique_id = 0;
                while ( exists $file_by_character{ $char_path } ) {
                    ++$unique_id;
                    $char_path = File::Spec->catdir( $realm, $character . "." . $unique_id );
                }
                $file_by_character{ $char_path } = $file;
            } ## end foreach my $character ( File::Spec...)
            $realm_dirh->close;
            undef $realm_dirh;
        } ## end foreach my $realm ( File::Spec...)
        $account_dirh->close;
        undef $account_dirh;
    } ## end foreach my $account ( File::Spec...)
    $accounts_dirh->close;
    undef $accounts_dirh;
} ## end sub get_character_lists( )

my %enabled_by_addon     = ();
my %enabled_by_character = ();

my $enabled_lists_read;

sub get_enabled_lists( ) {
    return if $enabled_lists_read;
    $enabled_lists_read = 1;

    get_character_lists unless $character_lists_read;

    # Process all the files
    while ( my ( $char, $file ) = each %file_by_character ) {
        my $addons = {};
        my $rec = $enabled_by_character{ $char } = {
            char    => $char,
            file    => $file,
            changed => 0,
            addons  => $addons,
            broken  => 1,
        };
        my $fh = new IO::File $file, "r";
        if ( $fh ) {
            while ( <$fh> ) {
                next unless m{^\s*(.*):\s*(enabled|disabled)\s*$};
                my ( $addon, $state ) = ( $1, $2 );
                $state = ( $state =~ m{^en} ? &STATE_ENABLED : &STATE_DISABLED );
                my $key = addon_name_to_key( $addon );
                $addons->{ $key } = [ $addon, $state ];
                my $addon_rec = ( $enabled_by_addon{ $key } ||= { name => $addon, characters => {} } );
                $addon_rec->{ "characters" }->{ $char } = $state;
                my $state_rec = ( $addon_rec->{ $state } ||= {} );
                $state_rec->{ $char } = $char;
            } ## end while ( <$fh> )
            close $fh
                and $rec->{ "broken" } = 0;
        } ## end if ( $fh )
    } ## end while ( my ( $char, $file...))

    # Determine default addons
    while ( my ( $key, $addon_rec ) = each %enabled_by_addon ) {
        my $addon      = $addon_rec->{ "name" };
        my $characters = $addon_rec->{ "characters" };
        while ( my ( $char, $char_rec ) = each %enabled_by_character ) {
            next if $characters->{ $char };
            $characters->{ $char } = &STATE_DEFAULT;
            $addon_rec->{ &STATE_DEFAULT }->{ $char } = $char;
            $char_rec->{ "addons" }->{ $key } = [ $addon, &STATE_DEFAULT ];
        } ## end while ( my ( $char, $char_rec...))
    } ## end while ( my ( $key, $addon_rec...))
} ## end sub get_enabled_lists( )

sub save_enabled_lists( ) {
    return unless $enabled_lists_read;

    # Process all the files
    while ( my ( $char, $rec ) = each %enabled_by_character ) {
        my $file       = $rec->{ "file" };
        my $write_file = $file . ".new";
        my $old_file   = $file . ".old";
        my $addons     = $rec->{ "addons" };
        next if $rec->{ "broken" };
        next unless $rec->{ "changed" };

        print "Writing: " . $file . "\n" if ( $opt_output{ "verbose" } && $opt_flags{ "dryrun" } );
        next if $opt_flags{ "dryrun" };

        my $fh = new IO::File $write_file, "w"
            or die "Error creating: " . $write_file . ": " . $! . "\n";

        binmode $fh, ":crlf";

        foreach my $key ( sort keys %$addons ) {
            my $rec = $addons->{ $key };
            next if ( $rec->[ 1 ] eq &STATE_DEFAULT );
            print $fh $rec->[ 0 ] . ": " . ( $rec->[ 1 ] eq &STATE_DISABLED ? "disabled" : "enabled" ) . "\n";
        }
        close $fh
            or die "Error writing: " . $write_file . ": " . $! . "\n";

        if ( -e $file ) {
            ::rmtree( $old_file );
            if ( -d $file ) {
                rename( $file, $old_file );
            }
            else {
                ::copy( $file, $old_file );
            }
        } ## end if ( -e $file )
        rename( $write_file, $file );
    } ## end while ( my ( $char, $rec ...))
} ## end sub save_enabled_lists( )

sub get_addon_enabled_disabled_default_count($ ) {
    my $addon = shift;
    get_enabled_lists unless $enabled_lists_read;
    my $key = addon_name_to_key( $addon );
    my $rec = $enabled_by_addon{ $key };
    return ( 0, 0, 1 ) unless ( $rec );
    my $enabled  = $rec->{ &STATE_ENABLED };
    my $disabled = $rec->{ &STATE_DISABLED };
    my $default  = $rec->{ &STATE_DEFAULT };
    $enabled  = scalar keys %$enabled;
    $disabled = scalar keys %$disabled;
    $default  = scalar keys %$default;
    return ( $enabled, $disabled, $default );
} ## end sub get_addon_enabled_disabled_default_count($ )

sub set_addon_state($$ ) {
    my ( $addon, $state ) = @_;
    get_enabled_lists unless $enabled_lists_read;
    my $key = addon_name_to_key( $addon );
    my $rec = $enabled_by_addon{ $key };
    unless ( $rec ) {
        $rec = { name => $addon, characters => {} };
        $enabled_by_addon{ $key } = $rec;
    }
    my $chars = $rec->{ "characters" };
    my $state_rec = $rec->{ $state } ||= {};
    delete $rec->{ &STATE_ENABLED }  unless $state eq &STATE_ENABLED;
    delete $rec->{ &STATE_DISABLED } unless $state eq &STATE_DISABLED;
    delete $rec->{ &STATE_DEFAULT }  unless $state eq &STATE_DEFAULT;

    while ( my ( $char, $char_rec ) = each %enabled_by_character ) {
        my $addons = $char_rec->{ "addons" };
        my $addon_key = $addons->{ $key } ||= [ $addon, &STATE_DEFAULT ];
        $char_rec->{ "changed" } = 1 unless ( $addon_key->[ 1 ] eq $state );
        $addon_key->[ 1 ]        = $state;
        $state_rec->{ $char }    = $char;
    } ## end while ( my ( $char, $char_rec...))
} ## end sub set_addon_state($$ )

# Argument parsing
sub map_argument($$$ ) {
    my ( $sub, $default, $ref ) = @_;
    my @in_choices = ( ref $ref ? ( @$ref ) : ( $ref ) );
    my $type       = undef;
    my @choices    = ();
    my %seen       = ();
    foreach my $choice ( @in_choices ) {
        $type = $1 if ( $choice =~ s{([+!]|[:=].*)$}{} );
        push @choices, $choice unless $seen{ $choice };
        $seen{ $choice } = 1;
        my $no_under = $choice;
        if ( $no_under =~ s{[_-]}{}g ) {
            push @choices, $no_under unless $seen{ $no_under };
            $seen{ $no_under } = 1;
        }
        my $as_dash = $choice;
        if ( $as_dash =~ s{_}{-}g ) {
            push @choices, $as_dash unless $seen{ $as_dash };
            $seen{ $as_dash } = 1;
        }
        my $as_under = $choice;
        if ( $as_under =~ s{-}{_}g ) {
            push @choices, $as_under unless $seen{ $as_under };
            $seen{ $as_under } = 1;
        }
    } ## end foreach my $choice ( @in_choices)
    $type = $default unless defined $type;
    $type = ""       unless defined $type;
    return join( "|", @choices ) . $type => $sub;
} ## end sub map_argument($$$ )

sub map_arguments($$@ ) {
    my $sub     = shift;
    my $default = shift;
    return ( map { ( map_argument( $sub, $default, $_ ) ) } @_ );
}

sub name_of_arguments(@ ) {
    return ( map { my $a = ( ref $_ ? $_->[ 0 ] : $_ ); $a =~ s{([+!]|[:=].*)$}{}; $a } @_ );
}

sub process_action_flag($$ ) {
    my ( $action, $value ) = @_;

    if ( $opt_callbacks{ $action } ) {
        return if $opt_callbacks{ $action }->( $action, $value );
    }
    if (   ( $action eq "enable" && $opt_actions{ "disable" } )
        || ( $action eq "disable" && $opt_actions{ "enable" } ) )
    {
        die "Only one of the actions -enable and -disable may be specified.\n";
    }
    $opt_actions{ $action } = $value;
} ## end sub process_action_flag($$ )

sub process_filter_option($$ ) {
    my ( $filter, $value ) = @_;

    if ( $opt_callbacks{ $filter } ) {
        return if $opt_callbacks{ $filter }->( $filter, $value );
    }

    # Split off comparison
    $value =~ s{^([<=>!]*)}{};
    my $comp = $1;

    # Normalize interface numbers
    if (   $filter eq qw{interface}
        && $value =~ m{^[\d.]*$} )
    {
        my @vals = split( /\./, $value );
        $value = ( ( $vals[ 0 ] || 0 ) * 100 + ( $vals[ 1 ] || 0 ) ) * 100 + ( $vals[ 2 ] || 0 ) + ( $vals[ 3 ] || 0 ) * 0;
    } ## end if ( $filter eq qw{interface}...)

    # Normalize the url
    if (   $filter eq qw{svn}
        || $filter eq qw{git}
        || $filter eq qw{hg} )
    {
        my $old_value = $value;
        my $uri       = URI->new( $value );

        # ($filter eq qw{git} ? "git" : "http"));
        if ( ( $uri->scheme || "" ) eq "" ) {
            if ( $filter eq qw{git} ) {
                $uri->scheme( "git" );
            }
            elsif ( $filter eq qw{hg} ) {
                $uri->scheme( "http" );
            }
            else {
                $uri->scheme( "svn" );
            }
        } ## end if ( ( $uri->scheme ||...))
        unless ( $uri->authority ) {
            my @path = $uri->path_segments;
            $uri->authority( shift @path );
            $uri->path_segments( "", @path );
        }
        $value = $uri->canonical->as_string;
    } ## end if ( $filter eq qw{svn}...)

    # Handle special case of revision being both a filter and a source
    if (   $filter eq qw{revision}
        || $filter eq qw{svn}
        || $filter eq qw{git}
        || $filter eq qw{hg} )
    {
        if ( $comp eq "" ) {
            process_source_option( $filter, $value );
            return if defined $opt_filters{ $filter };
        }
        elsif ( defined $opt_filters{ $filter } ) {
            unless ( $opt_filters{ $filter . "-op" } eq ""
                && $opt_filters{ $filter } eq $opt_source{ $filter } )
            {
                die "Option: -" . $filter . " was already set to: " . $opt_filters{ $filter . "-op" } . $opt_filters{ $filter } . "\n";
            }
        } ## end elsif ( defined $opt_filters...)
    } ## end if ( $filter eq qw{revision}...)
    elsif ( defined $opt_filters{ $filter } ) {
        die "Option: -" . $filter . " was already set to: " . $opt_filters{ $filter . "-op" } . $opt_filters{ $filter } . "\n";
    }

    $opt_filters{ $filter } = $value;
    $opt_filters{ $filter . "-op" } = $comp;
} ## end sub process_filter_option($$ )

sub process_path_option($$ ) {
    my ( $path, $value ) = @_;

    if ( $opt_callbacks{ $path } ) {
        return if $opt_callbacks{ $path }->( $path, $value );
    }

    if ( defined $opt_paths{ $path } ) {
        die "Option: -" . $path . " may only be specified once.\n";
    }
    if ( uc $value eq "%INSTALL%" ) {
        $value = &default_install_dir;
    }
    elsif ( uc $value eq "%STAGE%" ) {
        $value = &default_stage_dir;
    }
    $opt_paths{ $path } = $value;
} ## end sub process_path_option($$ )

sub process_source_option($$ ) {
    my ( $source, $value ) = @_;

    if ( $opt_callbacks{ $source } ) {
        return if $opt_callbacks{ $source }->( $source, $value );
    }

    if ( defined $opt_source{ $source } ) {
        die "Option: -" . $source . " was already set to: " . $opt_source{ $source } . "\n";
    }
    $opt_source{ $source } = $value;
} ## end sub process_source_option($$ )

sub process_output_option($$ ) {
    my ( $output, $value ) = @_;

    if ( $opt_callbacks{ $output } ) {
        return if $opt_callbacks{ $output }->( $output, $value );
    }

    $opt_output{ $output } = $value;
} ## end sub process_output_option($$ )

sub process_flag_option($$ ) {
    my ( $flag, $value ) = @_;

    if ( $opt_callbacks{ $flag } ) {
        return if $opt_callbacks{ $flag }->( $flag, $value );
    }

    $opt_flags{ $flag } = 1;
} ## end sub process_flag_option($$ )

sub process_string_option($$ ) {
    my ( $name, $value ) = @_;

    if ( $opt_callbacks{ $name } ) {
        return if $opt_callbacks{ $name }->( $name, $value );
    }

    $opt_strings{ $name } = $value;
} ## end sub process_string_option($$ )

# Main

my @allowed_actions           = ();
my %need_install_dir          = ();
my %need_read_staged_addon    = ();
my %need_read_installed_addon = ();
my %need_read_all_installed   = ();
my %need_process_addon        = ();
my %need_compare_addon        = ();

=head1 ACTIONS

At least of the following actions must be specified.

=over 16

=cut

push @allowed_actions, qw(archive=s);
$need_read_staged_addon{ "archive" } = 1;
$need_process_addon{ "archive" }     = 1;

=item B<-archive> I<file>

Creates an archive file containing the addons.

=cut

push @allowed_actions, [ qw(defembed def_embed default_embed) ];
$need_read_staged_addon{ "defembed" } = 1;

=item B<-defembed>

Sets the listed addons to embeds externals only if that is the default. If
no addons are listed, or the B<L<-all>> option is given, then all addons
are reset to the default embedding state. See also B<L<-embed>> and
B<L<-disembed>>.

=cut

push @allowed_actions, [ qw(defsplit def_split default_split) ];
$need_read_staged_addon{ "defsplit" } = 1;

=item B<-defsplit>

Sets the listed addons to split only if the default is to split. If no
addons are listed, or the B<L<-all>> option is given, then all the addons
are reset to the default split state. See also B<L<-nosplit>> and
B<L<-split>>.

=cut

push @allowed_actions, [ qw(diff compare) ];
$need_install_dir{ "diff" }          = 1;
$need_read_staged_addon{ "diff" }    = 1;
$need_read_installed_addon{ "diff" } = 1;
$need_process_addon{ "diff" }        = 1;
$need_compare_addon{ "diff" }        = 1;

=item B<-check-externals>

Check for unused externals in stage directory.

=cut

push @allowed_actions, [ qw(checkexternals check_externals) ];

=item B<-clean-externals>

Clean unused externals in stage directory.

=cut

push @allowed_actions, [ qw(cleanexternals clean_externals) ];

=item B<-compare>

Another name for the B<L<-diff>> option.

=item B<-diff>

Finds differences between the addons in the staging area and the ones
that have been installed in the game. The name of each changed addon is
printed following by a unified difference report. The comparision logic
is smart ennough to compare addons that have been split apart in the
installation directory.

=cut

push @allowed_actions, qw(disable);
$need_read_installed_addon{ "disable" } = 1;

=item B<-disable>

Disables the addons. For addons that are have modules that are split out
this only disables the named addon and not it's sub-modules.

=cut

push @allowed_actions, [ qw(disembed disembedded) ];
$need_read_staged_addon{ "disembed" } = 1;

=item B<-disembed>

Sets the listed addons to not fetch externals. If no addons are listed, or
the B<L<-all>> option is given, then the default is changed so that addons
will be not be embedded. See also B<L<-defembed>> and B<L<-embed>>.

=cut

push @allowed_actions, [ qw(disembedurl=s) ];
$need_read_staged_addon{ "disembedurl" } = 1;
$opt_callbacks{ "disembedurl" }          = sub {
    $opt_actions{ $_[ 0 ] }       = 1;
    $unforce_embed_url{ $_[ 1 ] } = 1;
    return 1;
};

=item B<-disembedurl> I<URL>

No longer force this URL to be embed even for disembedded addons.

=cut

push @allowed_actions, qw(dump);
$need_read_staged_addon{ "dump" } = 1;
$need_process_addon{ "dump" }     = 1;

=item B<-dump>

Dumps the listed addons in the internal representation. Used mostly to
debug B<addonpkg> itself.

=cut

push @allowed_actions, [ qw(editlocale edit_locale edit-locale) ];
$need_read_staged_addon{ "editlocale" } = 1;

=item B<-edit-locale>

Open the locale file and web page to allow cut and pasting.

=cut

push @allowed_actions, [ qw(embed embedded) ];
$need_read_staged_addon{ "embed" } = 1;

=item B<-embed>

Sets the listed addons to fetch external embeds directly into the addon.
If no addons are listed, or the B<L<-all>> option is given, then the
default is changed so that addons will be embedded. See also B<L<-defembed>>
and B<L<-disembed>>.

=cut

push @allowed_actions, [ qw(embedurl=s) ];
$need_read_staged_addon{ "embedurl" } = 1;
$opt_callbacks{ "embedurl" }          = sub {
    $opt_actions{ $_[ 0 ] }     = 1;
    $force_embed_url{ $_[ 1 ] } = 1;
    return 1;
};

=item B<-embedurl> I<URL>

Even when addon is disembed, embeds from this URL will be used.

=cut

push @allowed_actions, qw(enable);
$need_read_installed_addon{ "enable" } = 1;

=item B<-enable>

Enables the addons. For addons that are have modules that are split out
this only enables the named addon and not it's sub-modules.

=cut

push @allowed_actions, [ qw(externals) ];
$need_read_staged_addon{ "externals" } = 1;
$need_process_addon{ "externals" }     = 1;

=item B<-externals>

Lists the externals used by the named addons.

=cut

push @allowed_actions, [ qw(fetch get) ];

=item B<-fetch>

Retrieves the specified addons from an svn server and stores them in the
staging area. If the addon is already in the staging area it will be
updated. An internal database is used to determine from where to get new
addons. This may be set using the B<L<-svn|/-svn url>>, B<L<-git|/-git url>>
or B<L<-hg|/-hg url>> options. Fetching is done before any other action when actions
are combined.

=item B<-get>

Another name for the B<L<-fetch>> option.

=cut

push @allowed_actions, qw(help);

=item B<-help>

Displays a brief summary of how to use B<addonpkg>.

=cut

push @allowed_actions, qw(install);
$need_install_dir{ "install" }        = 1;
$need_read_staged_addon{ "install" }  = 1;
$need_read_all_installed{ "install" } = 1;
$need_process_addon{ "install" }      = 1;

=item B<-install>

Installs the addons specified from the staging area to the installation
directory. For addons such as B<BigWigs> and B<oRA2>, B<addonpkg> is will
automatically split out the nested addons.

=cut

push @allowed_actions, qw(list);
$need_read_staged_addon{ "list" } = 1;
$need_process_addon{ "list" }     = 1;

=item B<-list>

List addons from the staging directory. If the B<L<-details>>
option is given then details about each addon will be printed.

=cut

push @allowed_actions, qw(log);
$need_install_dir{ "log" }          = 1;
$need_read_staged_addon{ "log" }    = 1;
$need_read_installed_addon{ "log" } = 1;
$need_process_addon{ "log" }        = 1;
$need_compare_addon{ "log" }        = 1;

=item B<-log>

Displays SVN log of changes between staging directory and installation
directory.

=cut

push @allowed_actions, qw(man);

=item B<-man>

Displays this document.

=cut

push @allowed_actions, [ qw(nosplit no_split unsplit never_split dont_split do_not_split) ];
$need_read_staged_addon{ "nosplit" } = 1;

=item B<-nosplit>

Sets the listed addons to never be split when installed or upgraded. If no
addons are listed, or the B<L<-all>> option is given, then the default is
changed so that addons are not split. See also B<L<-defsplit>> and
B<L<-nosplit>>.

=cut

push @allowed_actions, [ qw(setalias set_alias=s) ];

=item B<-setalias> I<alias>

Makes an alias I<alias> referring to the listed addons. Not only the
addons listed directly are added to the alias, no filters are applied.
Unlike groups an an addon can be in more than one alias. Aliases and
groups within aliases are only expanded when they are used. Using an
alias implies all addons within it, even if they have not yet been
fetched.

=cut

push @allowed_actions, [ qw(setgroup set_group=s) ];
$need_read_staged_addon{ "setgroup" } = 1;

=item B<-setgroup> I<group>

Sets the group of the addons to I<group>. Unlike aliases, a addon may
be in only one group. Filters, aliases and other groups are expanded
before adding the addons to a group. Using an group name for any other
command only refers to the addons that have been fetched and are in
the staging area.

=cut

push @allowed_actions, [ qw(split can_split) ];
$need_read_staged_addon{ "split" } = 1;

=item B<-split>

Sets the listed addons to always be split when installed or upgraded. If no
addons are listed, or the B<L<-all>> option is given, then the default is
changed so that addons are split. See also B<L<-defsplit>> and
B<L<-nosplit>>.

=cut

push @allowed_actions, [ qw(uninstall remove) ];
$need_install_dir{ "uninstall" }       = 1;
$need_read_staged_addon{ "uninstall" } = 1;
$need_process_addon{ "uninstall" }     = 1;

=item B<-uninstall>

Uninstalls the addons specified from the the installation directory. For
addons such as B<BigWigs> and B<oRA2>, B<addonpkg> is will also uninstall
any split out addons. AddOns are not removed from the staging area.

=cut

push @allowed_actions, [ qw(update reinstall upgrade) ];
$need_install_dir{ "update" }        = 1;
$need_read_staged_addon{ "update" }  = 1;
$need_read_all_installed{ "update" } = 1;
$need_process_addon{ "update" }      = 1;

=item B<-update>

Reinstalls the addons specified from the staging area to the installation
directory but only if the addon has been installed beforce. For addons such
as B<BigWigs> and B<oRA2>, B<addonpkg> is will automatically split out
addons.

=cut

push @allowed_actions, qw(users);
$need_install_dir{ "users" }          = 1;
$need_read_staged_addon{ "users" }    = 1;
$need_read_installed_addon{ "users" } = 1;
$need_read_all_installed{ "users" }   = 1;
$need_process_addon{ "users" }        = 1;

=item B<-users>

List all the installed addons that use the named addons.

=cut

my @allowed_filters = ();

=back

=head1 Z<>OPTIONS

=head2 Filters

The following filters are supported. They restrict to which addons
any selected actions will be applied. When fetching a new addon
that is not yet in the staging directory it may not be possible
for B<addonpkg> to apply some of the filters below. Most of the
filters support an optional comparison such as =, <, <=, >, >=, !=
before their values.

=over 4

=item *

B<CAUTION: I<Be sure to use quotes around the arguments to
avoid your shell treating them as I/O redirections when using
comparisons.>>

=back

=over 16

=cut

push @allowed_filters, qw(author);

=item B<-author> [I<comparison>]I<name>

Limit the actions to addons written by I<name>.
The C<!> comparison can be used to exclude
addons by certain authors.

=cut

push @allowed_filters, qw(category);

=item B<-category> [I<comparison>]I<category>

Limit the actions to addons by addon category.
The C<!> comparison can be used to exclude
addons by category.

=cut

push @allowed_filters, [ qw(git git-url) ];

# =item Z<>B<-git> [I<comparison>]I<url>
#
# See B<-git> below in the source option section.
# Limit the actions to addons that were fetched from
# this GIT url. Or not fetched if C<!> is used as
# a comparision.
#
# =cut

push @allowed_filters, [ qw(hg hg-url) ];

# =item Z<>B<-hg> [I<comparison>]I<url>
#
# See B<-hg> below in the source option section.
# Limit the actions to addons that were fetched from
# this mercurial url. Or not fetched if C<!> is used as
# a comparision.
#
# =cut

push @allowed_filters, qw(installed!);

=item B<-installed>

Limits the command to addons that are installed, even if they are at
an older version. Note B<-update> is slightly different then
B<-install> B<-installed>. Using B<-update> will install subaddons even if
they are not present. Using B<-install> B<-installed> will not install
new subaddons.

=cut

push @allowed_filters, qw(interface);

=item B<-interface> [I<comparison>]I<number>

Limits the command to addons at interface level I<number>.
Interface numbers may be also be written as World of
Warcraft version numbers. For example: S<B<-interface> 'E<gt>=2.0'>

=cut

push @allowed_filters, [ qw(revision r) ];

=item B<-r> [I<comparison>]I<revision>

=item B<-revision> [I<comparison>]I<revision>

Limits the command to addons at the specified revision I<revision>.
When L<fetching|/-fetch> addons if no comparision was specified it
will attempt to fetch that revision. If this option is specified
twice, once with a comparison and once without; then the option with
the comparison will be used as a filter, and the other option will
be used to determine which revision to fetch.

=cut

push @allowed_filters, [ qw(svn svn-url) ];

=item Z<>B<-svn> [I<comparison>]I<url>

See B<-svn> below in the source option section.
Limit the actions to addons that were fetched from
this SVN url. Or not fetched if C<!> is used as
a comparision.

=cut

push @allowed_filters, [ qw(svn-only!) ];

=item B<-svnonly>

=item B<-svn-only>

This option causes B<addonpkg> to ignore any addons that were not
fetched with L<svn(1)> or by B<addonpkg L<-fetch>> using L<svn(1)>.
This option may be negated by using B<-nosyn>.

=cut

push @allowed_filters, qw(uninstalled!);

=item B<-uninstalled>

Limits the command to addons that are not currently installed.

=cut

my @allowed_paths = ();

=back

=head2 Paths

The following other options select the paths used by B<addonpkg>.
The special values B<%INSTALL%> and B<%STAGE%> may be used to specify
the default installition and staging directories with any of these
options.

=over 16

=cut

push @allowed_paths, [
    qw{directory
        dest-directory
        destination-directory
        d}
];

=item B<-d> I<dir>

=item B<-destination-directory> I<dir>

This specifies the default directory. If only one of
B<L<-stage-directory|/-stage-directory dir>> and
B<L<-install-directory|/-install-directory dir>> has been
specified I<dir> will be used for the other directory.
If no other directories have been specified and the
actions do not include: L<B<-diff>|/-diff>, or
B<L<-install>> then I<dir> will be used as the
L<staging directory|/-stage-directory dir> otherwise
it will be used as the
L<installation directory|/-install-directory>.

=cut

push @allowed_paths, [
    qw{install-directory
        installation-directory}
];

=item B<-installation-directory> I<dir>

=item B<-install-directory> I<dir>

This specifies the directory where addons will be installed with the
L<B<-install>|/-install> function or compared with the
L<B<-compare>|/-compare> function.
If this directory is not specified the standard addon directory
will be used. The environmental variable
B<L<ADDONPKG_INSTALL_DIR>> may be
used to override this default.


=cut

push @allowed_paths, [
    qw(stage-directory
        staging-directory
        source-directory
        src-directory
        s)
];

=item B<-s> I<dir>

=item B<-staging-directory> I<dir>

=item B<-stage-directory> I<dir>

This specifies the staging directory where addons will be placed
after being fetching from the SVN server and before being installed.
By default this will be the a directory called "B<StageArea>" that should
in the Interface directory next to the "B<AddOns>" directory.
The environmental variable B<L<ADDONPKG_STAGE_DIR>> may be used
to override this default.

=cut

my @allowed_source_opts = ();

=back

=head2 Source Repository

The following other options may also be specified. The full URL
used to fetch an addon is formed from the B<L<-svn|/-svn url>> option,

=over 16

=cut

#push @allowed_source_opts, qw(git);

=item B<-git> I<url>

The GIT url to fetch addons. The default of this option
is based on an addon database.
When addons are fetched the following substitutions are done:

#push @allowed_source_opts, qw(hg);

=item B<-hg> I<url>

The mercurial url to fetch addons. The default of this option
is based on an addon database.
When addons are fetched the following substitutions are done:

=for html <dir>

=over

=item B<%Name%>

The name of the addon.

=item B<%NAME%>

The name of the addon in upper case.

=item B<%name%>

The name of the addon in lower case.

=back

=for html </dir>

=cut

#push @allowed_source_opts, qw(revision);

=item B<-revision> I<revision>

The revision of the addon to fetch. This option may also
be used as a L<filter|/-revision [comparison]revision>.

=cut

#push @allowed_source_opts, qw(svn);

=item B<-svn> I<url>

The SVN url to fetch addons. If this option is not specified
the value currently in the addon database will be used.
When addons are fetched the following substitutions are done:

=for html <dir>

=over

=item B<%Name%>

The name of the addon.

=item B<%NAME%>

The name of the addon in upper case.

=item B<%name%>

The name of the addon in lower case.

=back

The B<L<-svn|/-svn [comparison]url>> option is also treated as a
filter and can be used to restrict actions to addons
from the particular SVN server.

=for html </dir>

=cut

my @allowed_output_opts = ();

=back

=head2 Output Options

The following options control output.

=over 16

=cut

push @allowed_output_opts, [ qw(columns=i width w) ];

=item B<-columns> I<width>

The width of the terminal. Used when printing the names of addons
with L<figlet(1)>. The defaults to the envirnoment variable
B<L<COLUMNS>> or 80 if that is not set.

=cut

push @allowed_output_opts, [ qw(describe! description!) ];

=item B<-describe>

Prints notes when listing addons. Implies the B<L<-list>> action.

=cut

push @allowed_output_opts, [ qw(details!) ];

=item B<-details>

Prints additional details when listing addons. Implies the B<L<-list>>
action.

=cut

push @allowed_output_opts, [ qw(font f) ];

=item B<-f> I<font>

=item B<-font> I<font>

The font used to print the addon names in difference reports using
L<figlet(1)>. Defaults to B<roman>.

=cut

push @allowed_output_opts, [ qw(lines=i rows) ];

=item B<-lines> I<lines>

The height of the terminal in lines. Defaults to the environmental
variable B<L<LINES>> or 24 if that is not set. When the B<L<-diff>>
action would output more then I<lines> lines the output will
be automatically paged, using the pager specified by the
environmental variable B<L<PAGER>>. If I<lines> is 0 then paging
will not be used. If I<lines> is 1 then paging will always
be used if there is any output.

=cut

push @allowed_output_opts, [ qw(other!) ];
$need_install_dir{ "other" }          = 1;
$need_read_installed_addon{ "other" } = 1;
$need_read_all_installed{ "other" }   = 1;

=item B<-other>

Display a list of installed addons that are not present in the stage area.

=cut

push @allowed_output_opts, [ qw(outdated!) ];
$need_install_dir{ "outdated" }          = 1;
$need_read_installed_addon{ "outdated" } = 1;
$need_compare_addon{ "outdated" }        = 1;

=item B<-outdated>

Display a list of installed addons that are older than the addons in the
staging area.

=cut

=item B<-rows> I<lines>

Alias for the B<L<-lines|/-lines lines>> option.

=cut

push @allowed_output_opts, [ qw(quickdiff!) ];

=item B<-quickdiff>

Only check embeds once when comparing.

=cut

push @allowed_output_opts, [ qw(verbose! v) ];

=item B<-v>

=item B<-verbose>

Prints additional verbose information as B<addonpkg> runs.

=cut

=item B<-w> I<width>

=item B<-width> I<width>

Alias for the B<L<-columns|/-columns width>> option.

=cut

my @allowed_string_opts = ();
my @allowed_flag_opts   = ();

=back

=head2 Other Options

The following other options may also be specified.

=over 16

=cut

push @allowed_flag_opts, [ qw(force!) ];

=item B<-force>

This option only applies to the B<L<-fetch>> action currently. By
default B<addonpkg> will only update existing addons in the staging
directory that have been previously fetched by L<svn(1)> or B<addonpkg>.
If the B<-force> option is specified any addon in the staging area
that is fetched but does not have L<svn(1)> information or has
different information will be renamed with a "B<.bak>" file extension
and rechecked out from L<svn(1)>.

=cut

push @allowed_flag_opts, [ qw(dryrun n dry-run!) ];

=item B<-n>

=item B<-dry-run>

=item B<-dryrun>

Only print the commands that B<addonpkg> would have run, but does not
fetch or install anything. The B<L<-diff>> action ignores this option.

=cut

push @allowed_flag_opts, [ qw(weak! weak_fetch) ];

=item B<-weak>

=item B<-weak-fetch>

Avoid fetching again if source was checked recently. Useful in reducing
load on servers when fetching several addons with libraries or embeds
separately since it won't try to fetch libraries or embeds again if it
recently tried. It is strongly recommended that you use this option if
you are running several fetch commands back to back.

=back

=head1 ADDONS

After the options you should list the addons you wish to install. Some
actions will assume all the addons in the staging directory if you do
not list any. If you use the B<L<-install>> action you must include the
B<-all> flag if you want to install everything.

=over 16

=cut

push @allowed_source_opts, qw(all!);

=item B<-all>

This is the same as specifying all the addons that are listed
in the staging directory. Note filters still apply even when
using all.

=cut

push @allowed_source_opts, qw(downloads! manual!);

=item B<-downloads>

This is the same as specifying all the addons that are listed
in the manual download directory.

=cut

push @allowed_source_opts, qw(libs!);

=item B<-libs>

This is the same as specifying all the addon libraries that
are used by the listed addons. It will check the optional
dependencies, and the X-Embeds of the TOC and the addon database
to determine which addons to fetch.

=item I<name>

An addon name without any slashes is treated as being in the
staging area. If you want to use a file in the current directory
as a path add a C<./> in front of it.

=item I<path>

An addon name with slash is treated as a path name to the addon.
It will not be mapped to the staging directory. You may also
specify the path to a zip file or tar archive containing one
or more addons.

=cut

GetOptions(    # Commands
    map_arguments( \&process_action_flag, "", @allowed_actions ),

    # Filters
    map_arguments( \&process_filter_option, "=s", @allowed_filters ),

    # Paths
    map_arguments( \&process_path_option, "=s", @allowed_paths ),

    # Repository (Source)
    map_arguments( \&process_source_option, "=s", @allowed_source_opts ),

    # Output
    map_arguments( \&process_output_option, "=s", @allowed_output_opts ),

    # Other
    map_arguments( \&process_flag_option,   "=s", @allowed_flag_opts ),
    map_arguments( \&process_string_option, "=s", @allowed_string_opts ),

    "ptr!"                => \$wow_ptr,
    "mac_mp3_workaround!" => \$mac_mp3_workaround,
) or pod2usage( 2 );
@opt_args = @ARGV;

pod2usage( 1 ) if ( $opt_actions{ "help" } );
pod2usage( -verbose => 2 ) if ( $opt_actions{ "man" } );

$opt_actions{ "list" } = 1 if $opt_output{ "details" };
$opt_actions{ "list" } = 1 if $opt_output{ "describe" };
$opt_actions{ "list" } = 1 if $opt_output{ "other" };
$opt_actions{ "list" } = 1 if $opt_output{ "outdated" };

unless ( %opt_actions ) {
    my @choices = sort ( name_of_arguments( @allowed_actions ) );
    my $last    = pop @choices;
    warn "At least one of -" . join( ", -", @choices ) . " or -" . $last . " must be specified.\n";
    pod2usage( 2 );
} ## end unless ( %opt_actions )
foreach my $table ( \%opt_actions, \%opt_output ) {
    foreach my $key ( keys %$table ) {
        if ( $table->{ $key } ) {
            $need_install_dir          = 1 if $need_install_dir{ $key };
            $need_read_staged_addon    = 1 if $need_read_staged_addon{ $key };
            $need_read_installed_addon = 1 if $need_read_installed_addon{ $key };
            $need_read_all_installed   = 1 if $need_read_all_installed{ $key };
            $need_process_addon        = 1 if $need_process_addon{ $key };
            $need_compare_addon        = 1 if $need_compare_addon{ $key };
        } ## end if ( $table->{ $key } )
    } ## end foreach my $key ( keys %$table)
} ## end foreach my $table ( \%opt_actions...)
$need_install_dir          = 1 if $opt_filters{ "installed" };
$need_install_dir          = 1 if $opt_filters{ "uninstalled" };
$need_read_installed_addon = 1 if $need_read_all_installed;

if ( defined $opt_paths{ "directory" } ) {
    if ( defined $opt_paths{ "install-directory" } ) {
        if ( defined $opt_paths{ "stage-directory" } ) {
            warn "Option -directory may not be specified if both\n-install-directory and -stage-directory have been specified.\n";
            pod2usage( 2 );
        }
        else {
            $opt_paths{ "stage-directory" } = $opt_paths{ "directory" };
        }
    } ## end if ( defined $opt_paths...)
    else {
        if ( defined $opt_paths{ "stage-directory" }
            || $need_install_dir )
        {
            $opt_paths{ "install-directory" } = $opt_paths{ "directory" };
        }
        else {
            $opt_paths{ "stage-directory" } = $opt_paths{ "directory" };
        }
    } ## end else [ if ( defined $opt_paths...)]
} ## end if ( defined $opt_paths...)
delete $opt_paths{ "directory" };

if ( $need_install_dir ) {
    $opt_paths{ "install-directory" } = default_install_dir unless ( defined $opt_paths{ "install-directory" } );
    die "Unable to determine destination, use -d <dir>\n" unless defined $opt_paths{ "install-directory" };
    die "Destination dir: " . $opt_paths{ "install-directory" } . ", does not exist.\n" unless -d $opt_paths{ "install-directory" };
}
$opt_paths{ "stage-directory" } = default_stage_dir unless ( defined $opt_paths{ "stage-directory" } );
$opt_paths{ "stage-directory" } = cwd unless ( defined $opt_paths{ "stage-directory" } );

if (( $opt_source{ "all" } || !@ARGV )
    && (   $opt_actions{ "disembed" }
        || $opt_actions{ "embed" }
        || $opt_actions{ "defembed" }
        || $opt_actions{ "split" }
        || $opt_actions{ "nosplit" }
        || $opt_actions{ "defsplit" } )
    )
{
    AddonInfo->load_database;

    # Change the default
    if ( $opt_actions{ "disembed" } ) {
        AddonInfo::disembed_externals( "" );
    }
    if ( $opt_actions{ "embed" } ) {
        AddonInfo::embed_externals( "" );
    }
    if ( $opt_actions{ "defembed" } ) {
        AddonInfo::set_externals_embedded( "", "default" );
    }
    if ( $opt_actions{ "split" } ) {
        AddonInfo::allow_split( "" );
    }
    if ( $opt_actions{ "nosplit" } ) {
        AddonInfo::disallow_split( "" );
    }
    if ( $opt_actions{ "defsplit" } ) {
        AddonInfo::set_split_allowed( "", "default" );
    }
    delete $opt_actions{ "disembed" };
    delete $opt_actions{ "embed" };
    delete $opt_actions{ "defembed" };
    delete $opt_actions{ "split" };
    delete $opt_actions{ "nosplit" };
    delete $opt_actions{ "defsplit" };

    unless ( %opt_actions ) {
        AddonInfo->save_database;
        exit( 0 );
    }
} ## end if ( ( $opt_source{ "all"...}))

# Get default addons
unless ( @ARGV ) {
    unless ( $opt_actions{ "install" }
        || $opt_actions{ "uninstall" }
        || $opt_actions{ "update" }
        || $opt_actions{ "setalias" }
        || $opt_actions{ "disable" }
        || $opt_actions{ "enable" }
        || $opt_actions{ "setgroup" }
        || $opt_source{ "downloads" } )
    {
        $opt_source{ "all" } = 1;
    } ## end unless ( $opt_actions{ "install"...})
} ## end unless ( @ARGV )

if ( $opt_source{ "all" } ) {
    push @ARGV, get_subdirs_in_dir( stage_dir, 1 );
}
if ( $opt_source{ "all" } || $opt_source{ "downloads" } ) {
    push @ARGV, get_downloads( stage_dir );
}
unless ( @ARGV ) {
    warn "No addons have been specified. Use -all if you wish to (un)install all the addons\nin your staging directory: " . stage_dir . "\n";
    pod2usage( 2 );
}

$opt_output{ "verbose" } = 1 if ( $opt_flags{ "dryrun" } && !defined $opt_output{ "verbose" } && !$opt_actions{ "diff" } );

if ( defined $opt_actions{ "setalias" } ) {
    my $name = $opt_actions{ "setalias" };
    AddonInfo->remake_alias( $name, @ARGV );
}

# Expand addon name aliases in arguments...
@ARGV = expand_addon_names( @ARGV );

### Convert arguments into addons
my @ADDON_PATHS = ();
while ( @ARGV ) {
    my $arg = shift @ARGV;

    #### TODO Treat as an explicit add to the database...
    #### If arg is svn://... assume svn
    #### If arg is git://... assume git
    #### If arg is http://... and has an extension assume ftp, otherwise svn
    if ( $arg =~ m{^(?:ht|f|tf)tps?:} ) {
        my $base = $arg;
        $base =~ s{.*/}{};
        my $target_dir = get_temp_path( "fetch" );
        my $src        = $arg;
        $arg = File::Spec->catfile( $target_dir, $base );
        print_command( "curl", "-o", $arg, $src ) if ( $opt_output{ "verbose" } || $opt_flags{ "dryrun" } );
        next if $opt_flags{ "dryrun" };
        my $fetched;
        my $req = HTTP::Request->new( GET => $src );
        my $res = $ua->request( $req );

        if ( $res->code == 200 ) {
            my $fh = new IO::File $arg, "w";
            if ( $fh ) {
                print $fh $res->content;
                $fetched = 1 if $fh->close;
                my $mtime = $res->last_modified;
                utime( $mtime, $mtime, $arg ) if ( $mtime );
            } ## end if ( $fh )
        } ## end if ( $res->code == 200)
        unless ( $fetched ) {
            print STDERR "Unable to fetch: " . $src . "\n";
            next;
        }
    } ## end if ( $arg =~ m{^(?:ht|f|tf)tps?:})
    my ( $path, $raw_name ) = AddonPath->lookup( $arg, stage_dir );
    unless ( $path ) {
        print STDERR "Internal warning: Can not figure out what path to use for: " . $arg . "\n";
        next;
    }
    push @ADDON_PATHS, [ $path, $raw_name ];
} ## end while ( @ARGV )

# Maybe process installed addons
if ( $need_read_all_installed ) {
    foreach my $addon ( get_subdirs_in_dir( install_dir ) ) {
        read_installed_addon( $addon );
    }
    foreach my $addon ( get_subdirs_in_dir( disabled_install_dir ) ) {
        read_installed_addon( $addon );
    }
} ## end if ( $need_read_all_installed)

if ( $opt_actions{ "checkexternals" } || $opt_actions{ "cleanexternals" } ) {
    get_paths_for_all_externals;
}

# Process stage addons
while ( @ADDON_PATHS ) {
    my $path_pair = shift @ADDON_PATHS;
    my ( $path, $raw_name );
    if ( ref( $path_pair ) eq q{ARRAY} ) {
        ( $path, $raw_name ) = @$path_pair;
    }
    else {
        $path     = $path_pair;
        $raw_name = undef;
    }
    push @ADDON_PATHS, read_staged_addon_path( $path, $raw_name );
} ## end while ( @ADDON_PATHS )

if ( $opt_actions{ "checkexternals" } || $opt_actions{ "cleanexternals" } ) {
    foreach my $addon ( expand_addon_names( get_subdirs_in_dir( stage_dir, 1 ) ) ) {
        my ( $addon_path, $raw_name ) = AddonPath->lookup( $addon, stage_dir );
        find_externals_for_addon( $addon_path );
    }

    foreach my $path ( sort keys %$externals_table ) {
        my $value  = $externals_table->{ $path };
        my $parent = $value->{ parent };
        next if $value->{ used };
        next if $parent && !( $parent->{ used } );

        if ( $opt_flags{ "dryrun" } || !$opt_actions{ "cleanexternals" } ) {
            print "Unused: ", $path, "\n";
        }
        else {
            rmtree( $path, $opt_output{ "verbose" } ) if ( !$opt_flags{ "dryrun" } && -e $path );
        }
    } ## end foreach my $path ( sort keys...)
} ## end if ( $opt_actions{ "checkexternals"...})

AddonInfo->save_database;
save_enabled_lists;

# Process installed addons if needed and was not handled above
if ( $need_read_installed_addon ) {
    foreach my $addon ( keys %$src_addon_table ) {
        read_installed_addon( $addon );
    }
    foreach my $addon ( keys %$given_addon_table ) {
        read_installed_addon( $addon );
    }
} ## end if ( $need_read_installed_addon)

my @outdated   = ();
my @all_addons = ();
{
    my %both = ( %$src_addon_table, %$dest_addon_table, %$given_addon_table );
    @all_addons = sort cmp_caseless keys %both;
}

my %users = ();
if ( $need_read_installed_addon ) {

    # Build a table of addons in install-directory
    if ( $opt_actions{ "users" } ) {
        while ( my ( $addon, $dest_addon ) = each %$dest_addon_table ) {
            my @uses = ();
            my $toc  = $dest_addon->get_toc;
            if ( defined $toc ) {
                push @uses, expand_addon_names( $toc->get_tag( &TOC_EMBEDS ) );
                push @uses, expand_addon_names( $toc->get_tag( &TOC_REQUIRED_DEPENDENCIES ) );
            }
            my $addon_info = $dest_addon->get_info;
            if ( defined $addon_info ) {
                push @uses, expand_addon_names( $addon_info->expand_require() );
            }
            foreach my $uses ( @uses ) {
                my $rec = $users{ $uses } ||= {};
                $rec->{ $addon } = 1;
            }
        } ## end while ( my ( $addon, $dest_addon...))
    } ## end if ( $opt_actions{ "users"...})
} ## end if ( $need_read_installed_addon)

if ( $need_process_addon && @all_addons ) {
    my $width = length( ( sort { length( $b ) <=> length( $a ) } @all_addons )[ 0 ] );
    my @list_output = ();
    my $only_once;
    if ( $opt_output{ "quickdiff" } ) {
        $only_once = {};
    }

    my %addons_by_external = ();

    foreach my $addon ( @all_addons ) {
        next_output_header( $addon );
        my $src_addon  = $src_addon_table->{ $addon };
        my $dest_addon = $dest_addon_table->{ $addon };
        my $changed    = undef;

        if ( $need_compare_addon ) {
            my @changed = compare_addon( $src_addon_table, $dest_addon_table, $addon, !$opt_actions{ "diff" }, $only_once );
            if ( @changed ) {
                $changed = 1;
                if ( $opt_actions{ "diff" } ) {
                    $exit_res = 1;
                    output @changed;
                }
                if ( $opt_actions{ "log" } ) {
                    output log_addon( $src_addon_table, $dest_addon_table, $addon );
                }
            } ## end if ( @changed )
        } ## end if ( $need_compare_addon)
        if ( $opt_actions{ "dump" } ) {
            output dump_addon( $src_addon_table, $addon ) if $src_addon;
        }
        if ( $opt_actions{ "list" } ) {
            my $include_addon;
            my $list_addon;
            if ( $opt_output{ "outdated" } ) {
                $include_addon = $changed;
                $list_addon    = $dest_addon;
            }
            elsif ( $opt_output{ "other" } ) {
                $include_addon = !defined $src_addon;
                $list_addon    = $dest_addon;
            }
            else {
                $include_addon = 1;
                $list_addon    = $src_addon;
            }
            if ( $include_addon ) {
                my $toc          = $list_addon && $list_addon->get_toc;
                my $parent       = $addon;
                my $parent_addon = $src_addon;
                while ( $parent_addon && $parent_addon->get_parent ) {
                    $parent       = $parent_addon->get_parent;
                    $parent_addon = $src_addon_table->{ $parent };
                }
                my $line  = $addon;
                my $space = ' ' x ( 1 + $width - length( $addon ) );
                my @rest  = ();
                if ( $opt_output{ "describe" } ) {
                    if ( $toc ) {
                        my $notes = $toc->get_tag( &TOC_NOTES );
                        push @rest, $notes if $notes;
                    }
                } ## end if ( $opt_output{ "describe"...})
                if ( $opt_output{ "details" } ) {
                    push @rest, "parent=" . $parent if ( $parent ne $addon );
                    my $state;
                    my $real_path = File::Spec->catdir( install_dir, $addon );
                    my $addon_path = AddonPath->lookup( $real_path, install_dir );
                    if ( $addon_path->is_disabled_dir ) {
                        $state = "DISABLEDMAX";
                    }
                    else {
                        my ( $enabled, $disabled, $default ) = get_addon_enabled_disabled_default_count( $addon );
                        my $toc_state = $toc && $toc->get_tag( "DefaultState" );
                        if ( $toc_state && $toc_state =~ m{disabled}i ) {
                            $disabled += $default;
                        }
                        else {
                            $enabled += $default;
                        }
                        $state = (
                              $enabled
                            ? $disabled 
                                    ? &STATE_MIXABLED 
                                    : &STATE_ENABLED
                            : $disabled ? &STATE_DISABLED
                            : &STATE_DEFAULT
                        );
                    } ## end else [ if ( $addon_path->is_disabled_dir)]
                    push @rest, $state;
                } ## end if ( $opt_output{ "details"...})
                if ( $opt_output{ "details" } ) {
                    if ( $opt_output{ "outdated" } || $opt_output{ "other" } ) {
                        unless ( $dest_addon ) {
                            push @rest, "not installed";
                        }
                    }
                } ## end if ( $opt_output{ "details"...})
                if ( $opt_output{ "details" } && $list_addon ) {
                    my $node_path = $list_addon && $list_addon->get_node_path;
                    if ( $opt_output{ "outdated" } || $opt_output{ "other" } ) {
                        my $src_toc       = $src_addon && $src_addon->get_toc;
                        my $src_node_path = $src_addon && $src_addon->get_node_path;
                        my $interface     = ( $toc     && $toc->get_tag( &TOC_INTERFACE ) )     || "?????";
                        my $src_interface = ( $src_toc && $src_toc->get_tag( &TOC_INTERFACE ) ) || "?????";
                        if ( $interface ne $src_interface ) {
                            my $piece = "interface=" . $interface;
                            $piece .= "(stage:" . $src_interface . ")"
                                if ( $src_interface ne "?????" );
                            push @rest, $piece;
                        } ## end if ( $interface ne $src_interface)

                        my $url = ( $node_path && $node_path->get_source_url );
                        my $name = ( $node_path && $node_path->get_name ) || $addon;
                        if ( $name eq $addon ) {
                            $url = unformat_string(
                                $url,
                                "name" => $addon
                            );
                        } ## end if ( $name eq $addon )
                        $url = "unknown" unless defined $url;

                        my $src_url = ( $src_node_path && $src_node_path->get_source_url );
                        my $src_name = ( $src_node_path && $src_node_path->get_name ) || $addon;
                        if ( $src_name eq $addon ) {
                            $src_url = unformat_string(
                                $src_url,
                                "name" => $addon
                            );
                        } ## end if ( $src_name eq $addon)
                        $src_url = "unknown" unless defined $src_url;

                        if ( $url ne $src_url ) {
                            my $piece = "url=" . $url;
                            $piece .= "(stage:" . $src_url . ")"
                                if ( $src_url ne "unknown" );
                            push @rest, $piece;
                        } ## end if ( $url ne $src_url )
                        my ( $rev, $changed );
                        ( $rev, $changed ) = $node_path->get_revision if ( $node_path );
                        my ( $src_rev, $src_changed );
                        ( $src_rev, $src_changed ) = $src_node_path->get_revision if ( $src_node_path );
                        $rev     = "?????" unless defined $rev;
                        $src_rev = "?????" unless defined $src_rev;
                        if ( $rev ne $src_rev ) {
                            my $piece = "revision=" . $rev;
                            $piece .= "(stage:" . $src_rev . ")"
                                if ( $src_rev ne "?????" );
                            push @rest, $piece;
                        } ## end if ( $rev ne $src_rev )
                        $changed     = "?????" unless defined $changed;
                        $src_changed = "?????" unless defined $src_changed;
                        if ( $changed ne $src_changed ) {
                            my $piece = "changed=" . $changed;
                            $piece .= "(stage:" . $src_changed . ")"
                                if ( $src_changed ne "?????" );
                            push @rest, $piece;
                        } ## end if ( $changed ne $src_changed)
                    } ## end if ( $opt_output{ "outdated"...})
                    else {
                        unless ( defined $opt_filters{ "category-op" }
                            && $opt_filters{ "category-op" } =~ m{^=*$} )
                        {
                            if ( defined $toc ) {
                                push @rest, "category=" . $toc->get_tag( &TOC_CATEGORY )
                                    if $toc->get_tag( &TOC_CATEGORY );
                            }
                        } ## end unless ( defined $opt_filters...)
                        unless ( defined $opt_filters{ "interface-op" }
                            && $opt_filters{ "interface-op" } =~ m{^=*$} )
                        {
                            if ( defined $toc ) {
                                push @rest, "interface=" . $toc->get_tag( &TOC_INTERFACE )
                                    if $toc->get_tag( &TOC_INTERFACE );
                            }
                        } ## end unless ( defined $opt_filters...)
                        if ( $parent eq $addon ) {
                            unless ( defined $opt_filters{ "svn-op" }
                                && $opt_filters{ "svn-op" } =~ m{^=*$} )
                            {
                                my $url = ( $node_path && $node_path->get_source_url );
                                my $name = ( $node_path && $node_path->get_name ) || $addon;
                                if ( $name eq $addon ) {
                                    $url = unformat_string(
                                        $url,
                                        "name" => $addon
                                    );
                                } ## end if ( $name eq $addon )
                                push @rest, "url=" . $url if $url;
                            } ## end unless ( defined $opt_filters...)
                        } ## end if ( $parent eq $addon)
                        unless ( defined $opt_filters{ "revision-op" }
                            && $opt_filters{ "revision-op" } =~ m{^=*$} )
                        {
                            my ( $rev, $changed );
                            ( $rev, $changed ) = $node_path->get_revision if ( $node_path );

                            #push @rest, "revision=".$rev if $rev;
                            push @rest, "revision=" . $changed if $changed;
                        } ## end unless ( defined $opt_filters...)
                    } ## end else [ if ( $opt_output{ "outdated"...})]
                } ## end if ( $opt_output{ "details"...})
                $line = $line . $space . join( " ", @rest ) if ( @rest );
                push @list_output, $line . "\n";
            } ## end if ( $include_addon )
        } ## end if ( $opt_actions{ "list"...})
        if ( $opt_actions{ "externals" } ) {
            if ( $src_addon ) {
                my $space = ' ' x ( 1 + $width - length( $addon ) );
                my $prefix = $addon . $space;
                my $any_print;
                my $node_path = $src_addon->get_node_path;
                my %externals = ();
                if ( !$node_path->get_addon_path->is_install_dir ) {
                    %externals = get_addon_externals( $src_addon->get_info, $node_path );
                }

                foreach my $path ( sort cmp_caseless keys %externals ) {
                    my $ext_url       = $externals{ $path };
                    my $ext_path      = get_path_for_external( $ext_url );
                    my $ext_ref       = ( $addons_by_external{ $ext_path } ||= {} );
                    my $ext_addon_ref = ( $ext_ref->{ $addon } ||= {} );
                    $ext_addon_ref->{ $path } = 1;
                    push @list_output, $prefix . $path . " => " . $ext_path, "\n";
                    $any_print = 1;
                    $prefix = ' ' x ( 1 + $width );
                } ## end foreach my $path ( sort cmp_caseless...)
                unless ( $any_print ) {
                    push @list_output, $prefix . "*none*\n" if $opt_output{ "verbose" };
                }
            } ## end if ( $src_addon )
        } ## end if ( $opt_actions{ "externals"...})

        if ( $opt_actions{ "users" } ) {
            if ( $src_addon ) {
                my $rec   = $users{ $addon };
                my $line  = $addon;
                my $space = ' ' x ( 1 + $width - length( $addon ) );
                my $print;
                if ( $rec ) {
                    $line = $line . $space . join( " ", sort cmp_caseless keys %$rec );
                    $print = 1;
                }
                else {
                    $line = $line . $space . "*none*";
                    $print = 1 if $opt_output{ "verbose" };
                }
                push @list_output, $line . "\n" if $print;
            } ## end if ( $src_addon )
        } ## end if ( $opt_actions{ "users"...})

        my $check_uninstall;
        if ( $opt_actions{ "install" } ) {
            if ( $src_addon ) {
                install_addon( $src_addon_table, install_dir, $addon );
            }
            elsif ( $dest_addon ) {
                $check_uninstall = 1;
            }
        } ## end if ( $opt_actions{ "install"...})
        if ( $opt_actions{ "update" } ) {
            if ( $src_addon ) {
                reinstall_addon( $src_addon_table, install_dir, $addon );
            }
            elsif ( $dest_addon ) {
                $check_uninstall = 1;
            }
        } ## end if ( $opt_actions{ "update"...})
        if ( $opt_actions{ "uninstall" } ) {
            if ( $src_addon ) {
                uninstall_addon( $dest_addon_table, install_dir, $addon );
            }
            elsif ( $dest_addon ) {
                $check_uninstall = 1;
            }
        } ## end if ( $opt_actions{ "uninstall"...})
        if ( $check_uninstall ) {
            my $toc = $dest_addon->get_toc;
            if ( defined $toc ) {
                my $parent = $toc->get_tag( &TOC_ADDONPKG_PARENT );
                if ( $parent ) {
                    my $src_parent = $src_addon_table->{ $parent };
                    if ( $src_parent ) {
                        uninstall_addon( $dest_addon_table, install_dir, $addon );
                    }
                } ## end if ( $parent )
            } ## end if ( defined $toc )
        } ## end if ( $check_uninstall )
    } ## end foreach my $addon ( @all_addons)

    if ( $opt_actions{ "externals" } ) {
        my $external_width = length( ( sort { length( $b ) <=> length( $a ) } keys %addons_by_external )[ 0 ] );
        foreach my $ext_path ( sort keys %addons_by_external ) {
            my $space = ' ' x ( 1 + $external_width - length( $ext_path ) );
            my $ext_ref = $addons_by_external{ $ext_path };
            push @list_output, $ext_path . $space . join( " ", sort cmp_caseless keys %$ext_ref ) . "\n";
        }
    } ## end if ( $opt_actions{ "externals"...})

    if ( @list_output ) {
        next_output_header;
        output @list_output;
    }
} ## end if ( $need_process_addon...)

if ( defined $opt_actions{ "archive" } && @all_addons ) {
    my $filename     = $opt_actions{ "archive" };
    my $abs_filename = File::Spec->rel2abs( $filename );

    # Need to create a zip archive...
    my ( undef, undef, $base ) = File::Spec->splitpath( $filename );
    $base =~ s{\.(.*)$}{};
    my $image_dir = get_temp_path( $base );
    my @installed = ();
    if ( $opt_flags{ "dryrun" } ) {
        @installed = @all_addons;
    }
    else {
        my @saved_comment = @to_comment;
        foreach my $addon ( @all_addons ) {
            my $src_addon = $src_addon_table->{ $addon };
            if ( defined $src_addon ) {
                install_addon( $src_addon_table, $image_dir, $addon, 1 );
                push @installed, $addon;
            }
        } ## end foreach my $addon ( @all_addons)
        @to_comment = @saved_comment;
    } ## end else [ if ( $opt_flags{ "dryrun"...})]
    my $cwd = cwd;
    chdir $image_dir;
    if ( $filename =~ m{\.zip$}i ) {
        my $zip_cmd = find_command( "zip" );
        unlink( $abs_filename ) unless ( $opt_flags{ "dryrun" } );
        if ( $zip_cmd ) {
            my @cmd = ( $zip_cmd, "-r" );
            push @cmd, "-q" unless $opt_output{ "verbose" };
            push @cmd, ( $abs_filename, @installed );
            print_command( @cmd ) if $opt_output{ "verbose" };
            do_command( @cmd ) unless ( $opt_flags{ "dryrun" } );
        } ## end if ( $zip_cmd )
        else {
            warn "Unable to find a zip command, so can not create archives.\n";
        }
    } ## end if ( $filename =~ m{\.zip$}i)
    elsif ( $filename =~ m{\.t(?:ar(?:\.(?:gz|bz2?|Z))?|gz|bz2?)$}i ) {

        # Need to create a tar archive...
        my $is_gz       = $filename =~ m{gz$}i;
        my $is_bz2      = $filename =~ m{bz2?$}i;
        my $is_compress = $filename =~ m{\.Z$}i;
        my $tar_cmd     = find_command( "tar" );
        if ( $tar_cmd ) {
            my @cmd = ( $tar_cmd, "-c" );
            push @cmd, "-v" if $opt_output{ "verbose" };
            push @cmd, "-Z" if $is_compress;
            push @cmd, "-z" if $is_gz;
            push @cmd, "-j" if $is_bz2;
            push @cmd, "-f", $abs_filename;
            push @cmd, @installed;
            print_command( @cmd ) if $opt_output{ "verbose" };
            do_command( @cmd ) unless ( $opt_flags{ "dryrun" } );
        } ## end if ( $tar_cmd )
        else {
            warn "Unable to find a zip command, so can not create archives.\n";
        }
    } ## end elsif ( $filename =~ m{\.t(?:ar(?:\.(?:gz|bz2?|Z))?|gz|bz2?)$}i)
    else {
        warn "Unrecognized archive format for: " . $filename . "\n";
    }
    chdir $cwd;
} ## end if ( defined $opt_actions...)

my $commenter = find_application( "Comment Wow Addon.app" );

if ( @to_comment && defined $commenter ) {
    my @cmd = ( "open", "-g", "-a", $commenter, @to_comment );
    print_command( @cmd ) if $opt_output{ "verbose" };
    do_command( @cmd );
}

finish_output;

exit $exit_res;

__END__

=back

=head1 DESCRIPTION

The B<addonpkg> script is designed to simplying get, checking and installing addons
from svn servers. Under normal usage the sequence would be to fetch updates for
all addons in the staging area, compare them with what has been installed and if
you think the changes are safe install them.

=head1 INSTALLATION

The latest version should be available from L<http://code.google.com/p/nayala-wow/>

=over 16

=item INSTALL PERL I<Windows only>

B<addonpkg> requires perl. If you do not already have a version of perl installed
you can get one at L<http://www.perl.org/get.html>. Mac OS X comes with perl already
installed.

=item INSTALL SUBVERSION

If you do not already have a command line version of SVN (Subversion) installed,
you can get one from L<http://subversion.tigris.org/project_packages.html>. Mac OS X
users can get a version from L<http://metissian.com/projects/macosx/subversion/> or
by using MacPorts (L<http://www.macports.org/>) or Fink (L<http://finkproject.org/>).

=item INSTALL ADDONPKG

Copy the addonpkg script to some place in your program path. For Mac users
B</usr/local/bin> is probably a good place.

For Windows users one of the easiest solutions is to rename it as addopkg.pl
and place it in your B<C:\Documents and Settings\I<AccountName>> directory.
Then just run it as 'B<addonpkg.pl ...>'. If you don't rename the script you can
instead run it as 'B<perl addonpkg ...>'.

=back

Additional programs that you may wish to install include figlet, colordiff
and less.

=head1 EXAMPLES

=over 8

=item addonpkg L<-fetch> L<-libs> addon1 addon2 group1

Fetches or updates addon1, addon2, all addons in group1 and any libraries needed
to the staging area.

=item addonpkg L<-diff> L<-libs> addon1 addon2

Shows the any changes that have occured in the libraries, addon1 or addon2.

=item addonpkg L<-install> L<-libs> addon1

Install all of the changed libraries and addon1.

=item addonpkg L<-d|/-d dir> '%INSTALL%' L<-archive|/-archive file> myaddons.zip

Creates an archive of all the installed addons into myaddons.zip.

=item addonpkg L<-archive|/-archive file> Share.zip L<-libs> addon1 addon2 addon3

Creaters an archive containing addon1, addon2, addon3 and all the libs they
use. This is useful for sharing addon packages with friends. Notice the
archive is created from the stage directory and not the installation directory.

=item addonpkg L<-fetch> L<-compare> L<-revision|/-revision revision> 19515 L<-lib> SmallToupees

Fetch revision 19515 of SmallToupees, and all the libraries, then
compare them against the ones currently installed.

=item addonpkg L<-list> L<-interface|/-interface [comparison]number> '<2.0'

List all addons in the staging area that are not yet ready for
WoW 2.0.

=back

=head1 ENVIRONMENT

The B<addonpkg> can use the following environmental variables.

=over 16

=item B<ACE_STAGE_DIR> I<(deprecated)>

The default staging directory. Use B<L<ADDONPKG_STAGE_DIR>> instead.

=item B<ADDONPKG_INSTALL_DIR>

The default addon installation directory, see the
B<L<-install-directory|/-install-directory dir>> I<dir> option.

=item B<ADDONPKG_STAGE_DIR>

The default staging directory, see the B<L<-s|/-s dir>> I<dir> option.

=item B<COLUMNS>

The width of the terminal in characters. Defaults to 80 if
not set.

=item B<LINES>

The number of lines of text visible in the terminal. Defaults
to 24 if not set.

=item B<PAGER>

The paging program used to display the output of the B<L<-diff>> action
if it is more than one screen.

=item B<PATH>

Used to look up the various programs B<addonpkg> uses to do it's jbo.

=back

=head1 FILES AND DIRECTORIES

The following files and directories are used by B<addonpkg>.

=over

=item B</Applications/World of Warcraft> I<(Mac)>

=item B<C:\Program Files\World of Warcraft> I<(Windows)>

The application directory for I<World of Warcraft>. B<addonpkg> will
attempt to find the installation even if it is installed somewhere else.

=item I<appdir...>B</Interface/AddOns> I<(Mac)>

=item I<appdir...>B<\Interface\AddOns> I<(Windows)>

The default addon installation directory for addons. See
B<L<-install-directory|/-install-directory dir>> and
B<L<ADDONPKG_INSTALL_DIR>> to override this directory. 

=item I<appdir...>B</Interface/StageArea> I<(Mac)>

=item I<appdir...>B<\Interface\StageArea> I<(Windows)>

The default addon staging directory for addons. See
B<L<-s|/-s dir>> and
B<L<ADDONPKG_STAGE_DIR>> to override this directory. 

=item I<stagedir>/B<.addonpkg>

Subdirectory off of the stage directory where B<addonpkg> stores
the database and caches of the various servers it knows about.

=item I<stagedir>/B<.addonpkg/addons>

Addon database. Generated automatically by B<addonpkg>. This
file describes from where various addons will be fetched. It
is rewritten each time B<addonpkg> runs.

=item I<stagedir>/B<.addonpkg/ls.>I<servername>

Cache of the available directories (addons) on a server.

=back

=head1 SEE ALSO

L<diff(1)>, L<figlet(1)>, L<less(1)>, L<more(1)>, L<open(1)>,
L<svn(1)>, L<tar(1)>, and L<unzip(1)>.

=head1 AUTHOR

Daphne Pfister

=head1 BUGS AND TODO

=over

=item *

The B<-all> option is currently assumed when no addons are listed
except for the B<-install> command.

=item *

Several filters are not applied when fetching addons for the first
time.

=item *

When failing during the fetch of an addon with the following error
message:

C<svn: PROPFIND request failed on ...>

B<addonpkg> should introduce a delay before fetching the next addon
to give the server breathing room and potentially retry that fetch
a limited number of times.

=item *

Support for fetching addons from normal FTP/HTTP servers should be
added, included addons that are stored in zip, tar, or tar.gz files.

=item *

Support for adding addons to the addon database should be added.

=item *

Tar files and zip files always require a path name even if located
in the current directory to be used as arguments.

=item *

Search for and submit additional bugs at:
L<http://code.google.com/p/nayala-wow/issues/list>

=back

=head1 LICENSE

Copyright (c) 2006-2008 Daphne Pfister

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

=cut
